Skip to content

The hook contract

A hook from .agents/hooks.json, and a tool’s bridged hook (run), speak Claude Code’s hook protocol in every agent. The adapter translates.

Your command runs through the shell (sh -c, or cmd /C on Windows) in the agent’s working directory, with CLAUDE_PROJECT_DIR set to the project’s root and the hook’s input on stdin. A hook in .agents/hooks.json stops after its timeout (60 seconds by default).

Your command gets Claude Code’s input on stdin, with the agent’s tool call named as Claude Code names it, plus harness and tool_kind:

{"hook_event_name": "PreToolUse", "session_id": "…", "cwd": "…",
"tool_name": "Bash", "tool_input": {"command": "rm -rf /"},
"harness": "codex", "tool_kind": "shell"}
Field Holds
hook_event_name SessionStart, SessionEnd, UserPromptSubmit, PreToolUse, PostToolUse, Stop, PreCompact
harness the agent that ran the hook: claude, codex, …
session_id, transcript_path, cwd when the agent gives them
prompt at prompt submit
tool_name, tool_input Claude Code’s: Bash with command as text; Read, Edit, Write with file_path; mcp__<server>__<tool>. A tool Claude Code does not have keeps its own name
tool_kind shell, read, write, mcp or other
tool_response after a tool
source at session start
stop_hook_active at stop: the agent is already continuing because a hook asked it to

Schema: hook-input.json.

Answer Session start Prompt Before a tool After a tool Stop
exit 2, the reason on stderr – block it block it reason to the agent keep going
{"decision": "block", "reason": "…"} – block it block it reason to the agent keep going
{"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": "…"}} – – block it – –
{"hookSpecificOutput": {"additionalContext": "…"}} add context add context – add context –
plain text on stdout add context add context – – –

Anything else, including exit 0 with no output, allows (at stop: lets the agent stop). “Keep going” sends the reason to the agent as its next input.

exit 2 is the hook protocol’s “block”; it is not the command’s own exit code 2 (a refused command line).

Schema: hook-answer.json.

In Claude Code your hook’s output passes through unchanged. In any other agent it is translated; what that agent cannot do (permissionDecision: "ask", updatedInput, continue: false, …) is left out with a note on stderr. The table on Hooks shows what each agent can do.

A command that cannot start, exits with another code than 0 or 2, or runs past its timeout allows, with a note on stderr. Your hook failing never blocks the agent.

The contract follows Claude Code’s hook protocol and changes only in a release that says so.