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/receipts
agent-custody init claude-code

Needs 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.

Claude Code · Bash
enforce mode
Denied
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)

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.

  1. 01
    Run the command

    It makes a key, a starter deny list, and the Claude Code hooks for the current directory.

  2. 02
    Work as usual

    In observe mode the hook blocks nothing. It records each shell command, file edit, and MCP tool call.

  3. 03
    Read the report

    It lists each call that the policy would have denied.

  4. 04
    Enforce

    Remove one line from the config. A denied call then stops before it runs.

agent-custody report --config ~/.config/agent-custody/sdk.json
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.

Runs

terraform plan

The hook has no objection. The command runs, and the hook records it.

Denied

terraform destroy -auto-approve

A rule of the deny list matches. The command does not run.

Denied

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/receipts
agent-custody init claude-code