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
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: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-inalias 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:
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
- In Claude Code and coding agents — the one-line settings entry, what comes back, what persists between events.
- With any agent SDK — wrapping the tools of the OpenAI Agents SDK, the Vercel AI SDK, a hand-rolled Anthropic tool loop, LangChain, an MCP server.
- Python SDK: hooks, TypeScript SDK: hooks — the API.