Skip to content

A Hook is an event-triggered script that fires when Claude Code does something — it has no goal of its own, only the power to allow, block, or observe.

Primitive

Hook

How a hook fires

stdin → decision → exit code. No conversation.

🤖

Agent

calls tool

📥

stdin (JSON)

event payload

🪝

Hook script

parses + decides

Exit code

0 / 1 / 2

The hook runs in a child process. The agent cannot see the hook's logic, cannot bypass it, cannot argue with the exit code.

Where a hook can fire

Seven events. Pick the one that matches your intent.

PreToolUse

Before any tool call. Can block the call from running.

can block

PostToolUse

After a tool call returns. Observes; can log or rewrite output.

observes

Stop

Agent about to emit Stop. Can block — verify contracts, force more work.

can block

SubagentStop

A spawned sub-agent stopping. Can block — validate before returning.

can block

UserPromptSubmit

User types a message. Can rewrite the prompt before agent sees it.

can block

PreCompact

Before /compact runs. Observes; archive context before it's summarized.

observes

Notification

Agent wants attention. Observes; route to Slack / TTS / desktop.

observes

Pick the earliest event that can see the thing you want to enforce. PreToolUse blocks; PostToolUse is too late to stop a mutation.

The only language a hook speaks

Three exit codes. Three meanings. No ambiguity.

0

allow

Green light. The tool call proceeds; the agent never knows the hook ran. Used for observation-only hooks and passed validations.

1

block silently

Red light, no message. The tool call is denied; the agent sees an error but no structured reason. Use sparingly — agents can't self-correct without a message.

2

block with message

Red light + stderr. Tool denied; the hook's stderr is passed to the agent as the block reason. The agent reads it and can retry correctly. The right choice for enforcement hooks.

Exit code 2 is the teaching signal — the agent learns from your stderr. Exit 1 is the silent deny. Exit 0 is the default.

The two kinds of hooks

Is this hook the judge, or the court reporter?

ENFORCE

Blocks when broken

The hook is the judge. It reads the payload, evaluates a contract, and returns 2 with a message if the contract fails. Agent cannot proceed.

# context-gate.sh

if ! required_reads_satisfied; then

echo "Read X before writing" >&2

exit 2

fi

AUDIT

Records what happened

The hook is the court reporter. Captures the event, appends to a log, exits 0. Never blocks. Used for context bundles, session tracking, metrics.

# context-bundle-capture.sh

echo "$INPUT" >> "$BUNDLE_FILE"

exit 0

One hook, one mode. Mixing enforce + audit in one script means a bug in the audit path can silently unblock the enforce path.

The most common bug

Hooks that do work instead of enforcement.

anti-pattern

"The hook will fix it for me"

A hook that rewrites the agent's output, generates content, or runs a build is doing work — not enforcing a contract. Hooks should allow, block, or observe. Nothing else. When a hook carries business logic, the agent has no way to know what actually ran, the hook becomes untestable in isolation, and every failure becomes a forensic investigation.

A hook that mutates is a skill with no name. Put the work in a Command, or an Agent. Keep the hook binary.

The discipline

A hook has no goal of its own.

It watches one event, enforces one contract, returns one exit code. That's the whole contract. When you need the hook to "do more," you don't need a bigger hook — you need a Command or an Agent. The value of the hook is its narrowness.