Screen Claude Code's built-in tools

Claude Code runs shell commands, file edits and web fetches itself. Those never become MCP messages, so a proxy sitting in front of your MCP servers does not see a single one of them. Two hook commands read the client's own tool calls and results and answer on stdout, against the same policy file, the same questions and the same log.

This is where the damage concentrates rather than a completeness exercise. Of the 100 hand-labeled calls in the benchmark, 37 are shell commands, and of the 51 dangerous ones, 21 are.

The configuration #

In ~/.claude/settings.json for every project, or .claude/settings.json for one.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell|Edit|Write|WebFetch",
        "hooks": [{ "type": "command", "command": "agent-chaperone hook pre" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash|PowerShell|Read|WebFetch",
        "hooks": [{ "type": "command", "command": "agent-chaperone hook post" }]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": "Bash|PowerShell|Read|WebFetch",
        "hooks": [{ "type": "command", "command": "agent-chaperone hook post" }]
      }
    ]
  }
}

The matchers are a starting point rather than a recommendation. Pre-tool screening is worth having on anything that changes state or sends data out. Post-tool screening is worth having on anything that brings in text from somewhere you did not write, which is where an injected instruction arrives.

Three details in there are not decoration. PowerShell is in both lists because on Windows, wherever that tool is enabled, the client routes shell commands through it and does not register Bash at all, so a matcher naming only Bash screens nothing there and fails silently. PostToolUseFailure is a separate event, and without it a failed tool's output is never screened, even though a fetch that failed still returned a body and a command that exited non-zero still printed. And Read sits in the post list but not the pre list, because the risk in reading a file is what the file says, not that it was read.

What each command answers #

agent-chaperone hook pre returns one of three things.

agent-chaperone hook post replaces the tool output when a result is withheld or annotated, and returns nothing when it passes. For a failed tool it adds context instead, because that event accepts context and nothing else. So a failed call's output can be annotated and never withheld, and the notice says so rather than implying the content was kept back.

That last one is the client's contract rather than a choice, and it is the reason the post screen is worth registering on both events even though only one of them can act.

What it reads from the environment #

Screening needs an API key in the environment the client launches hooks in. Without one the deterministic rules still run, which is the allow and deny lists and the secret patterns, and every judgment records that no model was asked.

A hook is launched with a fixed command line, so the choices a proxy takes as flags are taken from the environment here. Setting AGENT_CHAPERONE_STORE_CONTENT=0 keeps the judgments and drops the arguments and result text.

The policy file is read from ~/.config/agent-chaperone/policy.yaml, or from AGENT_CHAPERONE_POLICY when that is set. Built-in tools are recorded under the server name built-in, so a policy can name them the same way it names a server.

mode: shadow

servers:
  built-in:
    deny_tools: ['WebFetch']

What hooks cannot see #

Worth knowing before trusting the configuration above to cover file reads.

A hook fires on a tool call, so anything reaching the model without one is outside this entirely. The clearest case is a file you reference directly in your own message: the client inlines it into the prompt, no tool runs, no hook fires, and nothing here screens it or records it. The same goes for whatever the client loads at startup, including its own instruction files.

So the coverage is files the agent chose to read. It is not files you handed it. That distinction is easy to lose once the hooks are installed and the log starts filling up.

The other half is still MCP. Hooks cover the client's own tools, and the proxy covers the servers. Neither one covers the other, and running both is the configuration this was designed around.

Recording what you asked for #

One question the screens can ask is whether a call has anything to do with what you actually wanted, and it is the only one that needs something no tool call contains. Without a recorded task it is never sent at all.

agent-chaperone task "fix the login redirect, nothing outside src/auth"
agent-chaperone task            # read it back
agent-chaperone task --clear

The task is scoped to the working directory and believed for twelve hours, then ignored. A stale task is worse than none: the screen would judge today's calls against intent you have moved on from, and be confidently wrong rather than silent.

A client that already knows what you asked can record it without anyone typing. On Claude Code, a prompt-submit hook receives the prompt on stdin and hands it straight over.

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "jq -r .prompt | xargs -0 agent-chaperone task"
          }
        ]
      }
    ]
  }
}

Worth knowing before turning that on. Your prompt is then sent to the model backend with every screened call afterwards, which hands over more than a tool call alone would. The off-task threshold defaults high because a wrong answer here blocks work you asked for. Read your own log in shadow mode before letting it hold anything.

Other clients #

The entries above are for Claude Code, written against its published hook reference. Cursor documents a comparable hook system, and Codex has not been checked. Each client's contract differs in what a post-tool hook may replace, and that difference decides whether the post-result screen can withhold a result or only annotate it. Adding another client means reading that client's contract rather than assuming this one carries over.

The full hooks document, including how a replacement is matched to a tool's own output shape, is in the repository.