Skip to content

Blog Article

Claude Code hooks for verifiable handoffs

Use Claude Code hooks to record bounded tool events, verify results separately, and hand off evidence without treating a log as approval or leaking tool output.

By AppHandoff Team · Published · 11 min read

Illustration of coding agents exchanging a handoff record
Claude CodeEngineering

Claude Code hooks can help a coding team remember that a tool ran, but a hook log is not verification and it is not approval. The useful handoff pattern is to record a small, redacted event after a tool call, inspect the actual result, and then attach a human-readable proof summary to the work item that needs it. This guide shows a local Node fixture for the recording step and a checklist for the judgment that must follow. It is for developers coordinating coding work across sessions, not for turning shell output into an automatic release decision.

As of October 2026, Anthropic's Claude Code hooks reference documents event names, input fields, settings locations, and exit behavior; its hooks guide shows how to configure and test hooks. Both are worth checking before installation because this interface evolves. The example below is original and illustrative. Its valid and invalid stdin branches were exercised locally; it has not been installed in a live Claude Code session.

Choose the event for the job

For evidence capture, PostToolUse runs after a tool call succeeds at the tool layer, and PostToolUseFailure runs after a tool call fails. Both carry tool_name and tool_input; their other fields differ. A hook configured with matcher Bash limits the handler to Claude Code's Bash tool. Claude Code sends command-hook input as JSON on standard input. The settings file may be project-scoped at .claude/settings.json or local at .claude/settings.local.json. A project setting can be shared with collaborators, so review the script, file retention, and repository ignore rules before committing one.

Timing matters more than the hook name. PostToolUse and PostToolUseFailure happen after execution; neither can take the tool call back. On PostToolUse, exit code 2 shows stderr to Claude, but the tool has already run. If you genuinely need a pre-execution boundary, PreToolUse has structured output in the hookSpecificOutput object, with permissionDecision and permissionDecisionReason fields, returned with exit 0. Anthropic's decision-control reference is the source for those fields. Such a boundary still has to enforce the right policy; merely configuring a hook does not grant a person’s approval.

A small, redacted evidence recorder

Suppose a developer wants a local reminder that Claude Code used its Bash tool during a task. The following illustrative settings use the same handler for the two documented after-tool events. Save the script as .claude/hooks/evidence-log.mjs only after reviewing it for your environment. The settings snippet belongs inside the project settings object; merge it with existing settings instead of replacing them. The handler writes only an event name, a fixed tool name, a timestamp, and an explicit unknown verification result. It does not save command arguments, returned output, session identifiers, or error text.

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command",
        "command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/evidence-log.mjs\""}]
    }],
    "PostToolUseFailure": [{
      "matcher": "Bash",
      "hooks": [{"type": "command",
        "command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/evidence-log.mjs\""}]
    }]
  }
}

The code uses only Node's built-in modules. A one-megabyte input limit prevents an unexpectedly large tool response from consuming unbounded memory; such an event is rejected and will be absent from the log. A missing or malformed event exits nonzero without dumping the payload. Keep this file and its destination out of source control as appropriate. For a shared team workflow, decide who may read the log and when to delete it; local visibility is not the same as a durable team record.

// .claude/hooks/evidence-log.mjs (illustrative)
import { appendFileSync } from 'node:fs';
import { join } from 'node:path';

const chunks = [];
let bytes = 0;
for await (const chunk of process.stdin) {
  bytes += chunk.length;
  if (bytes > 1_000_000) process.exit(1);
  chunks.push(chunk);
}
let event;
try { event = JSON.parse(Buffer.concat(chunks).toString('utf8')); }
catch { process.exit(1); }
const allowed = ['PostToolUse', 'PostToolUseFailure'];
if (!allowed.includes(event?.hook_event_name) ||
    event.tool_name !== 'Bash' ||
    !event.tool_input || typeof event.tool_input !== 'object') {
  process.exit(1);
}
const project = process.env.CLAUDE_PROJECT_DIR;
if (!project) process.exit(1);
const record = {
  at: new Date().toISOString(),
  event: event.hook_event_name,
  tool: 'Bash',
  verification: 'unknown',
};
appendFileSync(join(project, '.claude', 'evidence.jsonl'),
  JSON.stringify(record) + '\n', { mode: 0o600 });

This script deliberately does not interpret tool_response. A successful Bash tool event is not a guarantee that every test within the shell command passed, and command output can include secrets or unrelated text. PostToolUseFailure gives an event-level failure signal, but the log still leaves verification unknown because a test verdict needs the actual command and its exit status. The log line is a prompt to investigate, not an audit certificate. If the append fails, the recorder also exits nonzero; on these after-tool events that failure cannot undo the preceding tool call.

Test the fixture without installing a hook

Before adding settings, copy the illustrative script into a temporary directory under .claude/hooks/ in a throwaway project and feed it controlled JSON. The two examples below use a fake command and fake response. Inspect the resulting evidence.jsonl: the valid case should create one line with verification: unknown; the invalid case should exit nonzero and add no line. Also test a PostToolUseFailure payload and check that it records only the allowed fields. This is a parser and redaction check, not proof that Claude Code loaded the settings or triggered the hook.

# From the throwaway project root, after saving .claude/hooks/evidence-log.mjs.
printf '%s\n' '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"printf fake"},"tool_response":{"secret":"DO_NOT_LOG"}}' | CLAUDE_PROJECT_DIR="$PWD" node .claude/hooks/evidence-log.mjs
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{}}' | CLAUDE_PROJECT_DIR="$PWD" node .claude/hooks/evidence-log.mjs
# The second invocation must exit nonzero and leave the record count unchanged.

To verify actual Claude Code wiring later, use the product's /hooks menu or documented debug path in a disposable session, run an innocuous tool call, and inspect the local file. Do not run this example against a production repository merely to prove the hook fires. Claude Code's testing guidance covers configuration inspection and debugging. If a matcher or settings source differs from your environment, adjust it after observing the product's behavior rather than treating this article's stdin fixture as an integration test.

Turn an event into useful handoff evidence

A useful handoff answers what changed, what check was run, what it actually returned, and what remains unresolved. The local log only supports the narrow statement that a Bash tool event was observed. A developer or authorized agent must inspect the relevant command result and identify the test name, observed exit status, failing cases, and revision tested before calling it verification. If a test did not run, say so. If output was truncated or a tool-level success hid a failing assertion, keep the result unknown until a targeted rerun resolves it.

For example, imagine a test command prints a green summary but the runner exits with a failure because a later setup step failed. A hook that searches the response for the word ‘passed’ would mislabel the run. The opposite error is possible when an unrelated warning appears beside a genuinely successful result. A concise handoff should identify the specific runner, command, exit status, and relevant output after inspection. If the output contains secrets, report the test name and verdict without copying the raw text. If there is no trustworthy exit status, preserve the uncertainty rather than guessing from prose. This is why the fixture always writes unknown.

  1. Identify the work item and exact revision or changed paths before attributing a check to a change.
  2. Use the local event record only to locate a possible verification step; inspect the command and its result separately.
  3. Record a concise test verdict with the actual exit status and relevant failure detail, or explicitly mark the result unknown.
  4. Remove secrets and personal fields from the summary, and keep raw command output in an access-controlled place if retention is needed.
  5. Ask the authorized person to approve a gated decision; do not promote a log entry into that decision.

For a cross-session work item, AppHandoff's MCP endpoint is https://api.apphandoff.com/mcp. The current MCP overview and connection guide explain the shared record. AppHandoff serves bootstrap, get, find, ticket, plan, message, project, and decide_lifecycle_proposal. An agent with access can resolve a project with bootstrap and read work with get or find; an authorized write may attach a reviewed summary to a ticket under the current rules. The decide_lifecycle_proposal action belongs to the signed-in human approval card, not to a model reading a hook log.

If multiple coding agents need the same context, see the Claude Code subagent guide for delegation boundaries and the cross-client coordination guide for shared work. An AGENTS.md generator can format standing instructions from the details you enter, such as how to report checks. Its output is deterministic and does not inspect the repository, install hooks, run tests, or verify compliance. The live evidence still comes from the observed run and a person or agent reviewing it.

Keep security and approval boundaries explicit

A hook executes with local capabilities, so review its code like any other automation. Never execute a command supplied in tool_input; it is data to inspect, and this example never reads it beyond checking that it is an object. Never write full payloads or tool responses to the record. They may contain access tokens, private file contents, customer data, or prompts from untrusted sources. The event file should have restricted access and a defined retention period. A project-shared hook also needs code review because every collaborator running that project may trigger it.

For preventive control, use the documented PreToolUse decision contract with a narrow rule and test both allowed and denied cases. Do not use a post-tool failure code to imply that a command never ran. For organizational approval, follow the actual approval path for the specific action, including project scope and current rule checks. Those are separate from Claude Code's local permission prompts. A permission to call a tool does not certify its output, and a verified test does not grant permission to merge, publish, or alter someone else's work.

What to keep from this pattern

Claude Code hooks are most useful here as small, predictable observation points. The original fixture shows the safe minimum: accept only two expected after-tool events, record fixed fields, and label verification unknown. Use a real tool run to check installation, then use actual command results to write a handoff another person can verify. If the hook disappears or its input grows beyond the bound, a missing log line should be treated as missing evidence, not as a successful check. The approval decision stays with the mechanism and person authorized for it.

Frequently asked questions

Can a PostToolUse hook approve a Claude Code tool call?

No. PostToolUse runs after the tool succeeds, so it cannot prevent or undo the call. A log from that event records an observation. PreToolUse can return a structured permission decision before execution, but a project approval still belongs to the person and system authorized to give it.

Does a PostToolUse Bash event prove my tests passed?

No. It shows that the tool call completed at Claude Code's tool level. A shell command can report a failing test within its response, and response formats differ by tool. Inspect the command's actual exit status and test output before recording a passing verification result.

Where should a handoff evidence hook store data?

A local file under the project can hold a small, redacted event trail if access and retention are appropriate. Keep command text, full tool responses, credentials, and personal fields out of the record. Review confirmed proof before attaching a summary to any shared work item.