- The agent ran
- terraform destroy -auto-approve
- Rule
- terraform destroy deletes infrastructure
- Result
- denied before it ran
- The agent was told
- agent-custody denied this call: terraform destroy deletes infrastructure (rule policy5, receipt 980d078f-2922-42b7-a5de-de3b99b00997)
Stop your coding agent from running destructive commands, and keep a signed record that a reviewer can check.
For engineers who run Claude Code against repositories and systems that touch production. One command adds a hook to Claude Code. The hook checks each shell command, file edit, and MCP tool call against a deny list before the call runs. The starter list covers force push, rm -rf on / and ~, terraform destroy, and drop database.
npm install -g @agent-custody/receiptsagent-custody init claude-codeNeeds Node 22. It starts in observe mode: nothing is blocked until you say so.
To try it once, run npx @agent-custody/receipts init claude-code. Do not keep the npx form: npm can clear its cache, and the hooks then stop working.
What you see
Work in Claude Code as usual. The hook signs a record of each shell command, file edit, and MCP tool call. The record is called a receipt. The report checks the signature of every receipt, and then it lists the calls that the deny list would have stopped.
- 01Run the command
It makes a key, a starter deny list, and the Claude Code hooks for the current directory.
- 02Work as usual
In observe mode the hook blocks nothing. It records each shell command, file edit, and MCP tool call.
- 03Read the report
It lists each call that the policy would have denied.
- 04Enforce
Remove one line from the config. A denied call then stops before it runs.
would have denied 2 calls (the policy recorded each one and blocked nothing)
Bash: git push --force origin main
reason: force push rewrites shared history (git push --force-with-lease is allowed)
rule: forbid(principal, action == Action::"Bash", resource) when { context.args has command && ( context.args.command like "*git push*--force *" || context.args.command like "*git push*--force" || context.args.command like "*git push -f*" || context.args.command like "*git push* -f" ) } [policy1]
receipt: 191e1095-db76-45fc-82d4-3dc9a6e14023 (2026-11-02T11:15:18.824Z)
Bash: terraform destroy -auto-approve
reason: terraform destroy deletes infrastructure
rule: forbid(principal, action == Action::"Bash", resource) when { context.args has command && context.args.command like "*terraform destroy*" } [policy5]
receipt: 6f4c2455-b578-4a0f-b7ed-7fee177e5a9b (2026-11-02T11:15:18.827Z)
Checked 4 receipts: 4 verified, 0 did not verify.
To enforce this policy, remove the "mode": "observe" line from ~/.config/agent-custody/sdk.json. A denied call is then blocked before it runs.Output of tutorial 36, with the path of sdk.json shortened. The tutorial runs four calls: two match the list.
An incident replay
A coding agent runs terraform destroy. This is the moment that you want the hook for. A script plays the session in enforce mode. It needs no model and no network, and it runs no destructive command.
terraform plan
The hook has no objection. The command runs, and the hook records it.
terraform destroy -auto-approve
A rule of the deny list matches. The command does not run.
rm -rf ~
A second rule matches. The command does not run.
What Claude Code shows the agent
agent-custody denied this call: terraform destroy deletes infrastructure (rule policy5, receipt 980d078f-2922-42b7-a5de-de3b99b00997)
The text says what the rule forbids. It names the rule, policy5, and the id of the signed record of the denial.
What a reviewer checks, offline
6dd02745-62bd-46fa-9866-2e51adc0f682 VERIFIED (13 checks) executed decision allow terraform plan 980d078f-2922-42b7-a5de-de3b99b00997 VERIFIED (12 checks) denied decision deny terraform destroy -auto-approve 095d119e-5da7-4c74-bcc1-c91f0f2873c4 VERIFIED (12 checks) denied decision deny rm -rf ~ the same receipt checked against a key that is not the agent's does not verify receipt signature (issuer key): FAIL. RESULT: NOT VERIFIED
The reviewer holds the agent's public key and the receipt files. Nothing else. The last line is the terraform destroy receipt checked with a key that is not the agent's. Verify a receipt in your browser →
What the hook does not stop
The hook stops a shell command, a file edit, or an MCP tool call that the agent makes. Read these limits before you rely on it.
A credential in the environment
If the agent's shell holds a cloud key or a database password, a process that the agent starts can use it. The hook stops a call that the agent makes through its tools. It does not stop that process.
The match is on text
The starter list matches the text of a command. It catches the plain form of each command. It is not a sandbox.
Some tools are not seen
The hook matches shell commands, file edits, and MCP tools. It does not see Read, Grep, Glob, WebFetch, WebSearch, or Task. The starter deny list has rules for shell commands only.
The record is the agent's own report
The signature shows that the record did not change after the hook wrote it. It does not show that the hook saw every call.
To cover more, keep production credentials out of the agent's environment, and put a gateway between the agent and the tools that touch production. The gateway is on the advanced page. The threat model lists what each part defends, and what it does not. No independent witness runs yet, and the hosted log is not anchored on mainnet yet.
Try it on your own sessions
Needs Node 22. No account. A global install keeps the hooks working.
npm install -g @agent-custody/receiptsagent-custody init claude-code