> ## Documentation Index
> Fetch the complete documentation index at: https://docs.surfacearea.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Hooks for any agent

> One rules file around an agent's tool calls — deny, rewrite, add context, record, and put a persona in front of every private individual — attached to any agent SDK, or run as a coding agent's hook.

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:

| Where the agent runs | How the rules attach | What a rule can do there |
| - | - | - |
| Code you wrote on an agent SDK (Python or TypeScript) | wrap the tools once — [With any agent SDK](/hooks/agent-sdks) | deny, rewrite input **and result**, add context, record, alias door both ways |
| A coding agent you configure (Claude Code, and the agents that copied its hook contract) | `gateway hooks run` as the agent's hook command — [In Claude Code and coding agents](/hooks/coding-agents) | deny, rewrite input, add context, record, alias door **outbound** |
| A hook you already wrote for Claude Code | a `command` handler in the file, unchanged | whatever it did |

The file, the events and the handler contract are the same everywhere. The
[TypeScript SDK page](/sdk-ts/hooks) documents them in full; the [Python page](/sdk/hooks) 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

```toml theme={null}
[alias]
slugs = ["spycloud"]                   # worlds that store personal fields as sha256:<slug>:<value>

[vendors]
spycloud = "spycloud_.*"               # tool-name patterns -> a vendor label for `vendor = ...` matchers

[[hooks.pre_tool_call]]                # or [[hooks.PreToolUse]] — Claude Code's names are accepted
matcher = "spycloud_.* | web_search"   # top-level `|` alternates unanchored regexes; "" or "*" is every tool
handlers = [
  { type = "alias" },                                              # persona -> real on the way out
  { type = "command", command = "python guard.py", timeout = 10 }, # your hook: event on stdin, verdict on stdout
  { type = "deny", reason = "not in this run" },
]

[[hooks.post_tool_call]]
matcher = "spycloud_.* | web_search"
handlers = [
  { type = "alias" },                                              # real -> persona on the way in
  { type = "record", sink = "runs/{run_id}/ledger.jsonl" },        # every call, args in persona space
]

[[hooks.run_end]]                       # or [[hooks.Stop]]
handlers = [{ type = "alias", check = "outgoing" }]                # fail the run if a real value leaves in the report
```

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:

```json theme={null}
// in (stdin / request body / argument)
{ "hook_event_name": "pre_tool_call", "run_id": "…", "tool_name": "web_search",
  "tool_input": { "query": "Keith Yarrow" }, "vendor": "web", "matcher": "web_search", "cwd": "…" }

// out (stdout / response body / return value)
{ "decision": "allow" | "deny" | "rewrite", "reason": "…", "updated_input": { … }, "additional_context": "…" }
```

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:

```json theme={null}
[{ "type": "person_name", "value": "Matthew Nowell", "identity": "subject" },
 { "type": "email", "value": "m.nowell@nowell-ltd.com", "identity": "subject" },
 { "type": "domain", "value": "star-co.net.kp", "role": "public" }]
```

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:

```toml theme={null}
[alias]
mint_inbound = ["email", "phone", "ip"]   # a value of these kinds met in a result gets a persona on sight

[[hooks.post_tool_call]]                  # its own rule: one rewriter per rule, and rules chain
matcher = "web_search | web_fetch"
handlers = [{ type = "redact", kinds = ["phone"], patterns = ["\\bDOB[: ]+\\S+"], token = "[redacted {kind}]" }]
```

`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:

```bash theme={null}
gateway hooks legend runs/<run id>/            # persona -> real, with role, provenance, identity
gateway hooks reveal runs/<run id>/report.md   # the report with every persona put back
gateway hooks reveal transcript.jsonl --legend .gateway/hooks/<session>/legend.json --out transcript.real.jsonl
```

`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

```bash theme={null}
gateway hooks check gateway-hooks.toml                           # validate, build every handler, dry-run one event
gateway hooks check gateway-hooks.toml --seed @entities.json --tool web_search --input '{"query":"Keith Yarrow"}'
```

## Where next

* [In Claude Code and coding agents](/hooks/coding-agents) — the one-line settings entry, what
  comes back, what persists between events.
* [With any agent SDK](/hooks/agent-sdks) — 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](/sdk/hooks), [TypeScript SDK: hooks](/sdk-ts/hooks) — the API.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.