Skip to main content
A hook is a rule that runs around one tool call, the way a Claude Code hook runs around one tool use. The rules live in one file next to the agent, gateway-hooks.toml (JSON is accepted), and the same file runs in three places: The file, the events and the handler contract are the same everywhere. The TypeScript SDK page documents them in full; the Python page covers that surface. Every behaviour below is a rule you wrote in the file or a flag with a default.

The file, in one screen

Events: pre_tool_call, post_tool_call, pre_request, post_response, run_end. Handler types: alias, command, http, callable, deny, record. Rules for one event run in file order; per matcher the merge is deny over rewrite over allow, at most one rewriter.

The handler contract

Your hook — a command, an HTTP endpoint or a function — gets the event and answers a verdict:
Claude Code’s hookSpecificOutput form (permissionDecision, updatedInput, additionalContext) is accepted too, and exit code 2 with stderr is a deny. A hook written for Claude Code runs unchanged; a hook written for these rules runs in Claude Code through gateway hooks run.

The alias door

The built-in alias handler is the reason the rules exist: every private individual in a task gets one consistent persona, the model works entirely in persona space, the real value is what reaches a vendor or the web, and every answer comes back rewritten. The door is metered — a real value the model saw (leak-through in), a real value in the report (leak out), a persona that left unresolved — and run_end with check = "outgoing" fails the run on the last two. Seed it with the task’s entities before the agent starts:
Kinds: person_name, email, username, phone, company, domain, ip, account, machine, log; anything else gets a generic REF-nnnn stand-in. role: "public" marks a value that is never replaced, and by default the public layer wins even inside a private value’s derived variants ([alias] public_layer_wins, public_mailbox_domains).

Identities that arrive, not seeded

A page or a vendor row brings people the seed list never named — a WHOIS contact, a byline, a number on a profile. By default they reach the agent as they are: the seed list is the whole map. Two knobs change that, per task, never by default:
mint_inbound makes a found value a persona like a seeded one — aliased in, resolved back to the real value on the next call out, one persona for the rest of the run whatever the spelling — and lists each in found_inbound. email, phone and ip are found by pattern (a public IP; a number with a + or an area code); a name or a handle is seeded, never guessed. The whole result is read before any of it is rewritten, so a spelling in an earlier field (a profile URL carrying the address percent-encoded) is caught by a later one. redact removes instead of replacing, for anything the agent must never see, from any string a result carries — a page body, a title, a URL, a vendor row: kinds for shaped values, values for arbitrary literals (a name, a company, a phrase; matched in any case and across any whitespace), patterns for regular expressions. It runs after the door and leaves personas alone; every removal is counted in redactions with the kind and a three-letter prefix, never the value.

Seeing the real data (the owner)

The door hides values from the agent, not from you. With [alias] legend = "{run_dir}/legend.json" the run writes its legend at run_end — the run key and every persona, found ones included — and a coding agent’s state dir keeps one beside run.json after every event. Then:
reveal walks JSON or text and puts every persona spelling back; a [redacted …] token stays, because redaction removed the value — its raw copy is a record sink placed before the redact rule. The legend is what maps personas to people: keep it with the run key, out of version control, and never beside world data. What a task aliases is the task’s decision, so the run overrides the file: HookRun.from_config(path, mint_inbound=["email"]) / HookRun.fromConfig(path, { mintInbound: [] }), run.seed(...) with that task’s entities and roles, and run.on("post_tool_call", RedactHandler(...)) for a redaction only this task wants. run.mint(kind, value) adds one persona from code at any point of the run.

Check a file

Where next