Claude Code Hooks: Wetin Dem Dey Do and How E Work
Claude Code hooks dey run whether model agree or no. Learn where dem live, event names, stdin JSON, and how exit code 2 fit stop tool call.
Wetín be Claude Code hook
Claude Code hooks na shell commands wey Claude Code dey run by itself for fixed points inside e own lifecycle. Na this one be the main difference between hook and rules file. Instruction inside CLAUDE.md na advice, and model dey weigh am against every other thing wey dey inside e context. Hook na code, and e go run whether model agree or no. If your agent dey continue to skip formatter wey you don tell am about two times, you no need stronger instruction. You need hook.
The mechanism small. You register command inside settings file under event name. When that event fire, Claude Code run your command and write event data to e standard input (stdin) as JSON (JavaScript object notation). Your command read that data, do the work, then answer with exit status. Exit 2 from PreToolUse hook cancel the tool call before e run, and anything wey your script write to standard error (stderr) go return to model as the reason.
Event names and field names for here come from Claude Code hooks reference, wey dem check for August 2026 against release 2.1.232. This surface dey change quickly, so check the reference for your own version before you copy JSON from any blog post, including this one. Print your own with claude --version.
Wey hook configuration dey
Hook na JSON block wey dey inside settings file. Six places fit hold one, and scope of the file na scope of the hook.
~/.claude/settings.json: every project for your machine, and nobody else own..claude/settings.json: one project, committed to the repository, so everybody wey clone am go get the hook..claude/settings.local.json: one project, na your machine only.- Managed policy settings: e cover the whole organisation, and administrator dey set am.
hooks/hooks.jsoninside plugin, e dey active as long as plugin dey enabled.- Skill or subagent frontmatter, e dey active as long as that component dey active.
Hook entries from these files dey merge; dem no dey override each other. Project settings file adds its hooks to the ones for your user settings instead of replacing dem. So, one event fit get several hooks from different files. Setting "disableAllHooks": true switches dem off. But hooks from managed policy settings keep running unless you apply that setting inside managed settings too.
Run /hooks inside a session to list every hook wey currently registered. E go group dem by event, and show source file plus matcher for each one. The menu na read only, so edit the settings file to change a hook. File watcher usually go detect the edit without restart.
Claude Code hook event dem dey
Release 2.1.232 list thirty one events, from SessionStart reach SessionEnd. Dem cover compaction, subagents, worktrees, and configuration files. Server work only need some of dem.
PreToolUse: before tool call execute. Na this one fit block.PostToolUse: after tool call succeed.PostToolUseFailurego fire instead when e fail. So, if hook must see every result, e need both.PermissionRequest: when tool call need permission decision. Na that time approval prompt for show.UserPromptSubmit: when you submit prompt, before Claude process am. Anything wey this hook print to stdout go enter model context.SessionStartandSessionEnd: for each end of session.SessionStartgo also fire after compaction, under matcher valuecompact.Stop: when Claude finish response. This one na once for each turn, no be once for each task wey finish.
Every group get matcher wey decide which occurrences go run the hook. For tool events, e filter by tool name. So "Edit|Write" go fire for file edits and no other thing. Matchers dey case sensitive. Empty matcher go fire for every occurrence. Tools from MCP (model context protocol) server get name mcp__<server>__<tool>. So matcher "mcp__github__.*" go catch tools from one server and leave the other ones alone.
Stop hooks get one trap wey you need know before you write one. Stop hook wey block go send the model back to work, and Claude Code go override the hook after eight blocks one after another. Read stop_hook_active field from hook input and exit 0 when e true. If you no do am, your hook go loop until e reach that limit.
Wetin hook dey receive for stdin
When Claude wan run npm test, a PreToolUse hook for Bash go read this for stdin:
{
"session_id": "abc123",
"cwd": "/home/deploy/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm test"
}
}Every event carry session_id, cwd, permission_mode, transcript_path and hook_event_name. The tool events add tool_name, tool_input and tool_use_id. Other events carry their own fields: UserPromptSubmit get the prompt text, while SessionStart get a source of startup, resume, clear or compact, or fork.
jq na the usual way to read this inside a shell script, and minimal server image no get am. Install am first with sudo apt install -y jq for Ubuntu and Debian.
Wetin exit status dey do to tool call wey dey run
Outcomes dey three.
- Exit 0 mean say your hook no object. For
PreToolUse, this no mean approval, and the normal permission flow still dey run. ForUserPromptSubmitandSessionStart, stdout dey added to the model context. - Exit 2 block the action for events wey fit block,
PreToolUsejoin, and stderr become the reason wey model go see. For events wey no fit block, likePostToolUse, the block no get effect, but stderr still reach the model as feedback. - Any other exit code na error wey no block anything. The action go continue. The transcript go show hook error notice wey carry the first line of stderr after the text
Failed with non-blocking status code:.
If you need anything pass blocking or staying quiet, use exit 0 and print JSON object to stdout instead. A PreToolUse hook dey decide with permissionDecision:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Database drops go through a migration, not through the agent."
}
}"allow" skip the interactive prompt, "deny" cancel the call and send the reason to the model, and "ask" show the prompt normally. Choose one style for each hook. If you mix exit 2 with JSON decision for stdout, you go need check documentation to know the result.
When several hooks match one event, dem dey run in parallel and every one go run reach completion. A deny from one hook no stop the other hooks, so logging hook still write its line while guardrail hook deny the same call. Claude Code go then merge the answers and keep the most restrictive one, for this order: deny, defer, ask, allow.
Example 1: block destructive command before e run
Save this as .claude/hooks/block-destructive.sh for your project:
#!/bin/bash
# Deny a Bash tool call whose command matches a banned pattern.
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
for pattern in 'rm -rf /' 'mkfs' 'dd if=' 'DROP TABLE'; do
if printf '%s' "$COMMAND" | grep -qiF -- "$pattern"; then
echo "Blocked by policy: the command matches '$pattern'. A human runs this one." >&2
exit 2
fi
done
exit 0Make am executable, then register am for PreToolUse inside .claude/settings.json:
chmod +x .claude/hooks/block-destructive.sh{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10,
"statusMessage": "Checking the command against policy"
}
]
}
]
}
}Test the script by hand before you trust am, because if hook crash when e process im own input, e go fail open:
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /var/lib/postgresql"}}' \
| .claude/hooks/block-destructive.sh
echo $?You suppose see the Blocked by policy: line for stderr and exit code of 2. Feed am harmless command like ls -la, and you suppose see no output with exit code of 0. For a session, the denied call go show for the transcript with your message as the reason, and the model go read the message and adjust.
One thing make this worth doing: PreToolUse hooks dey fire before permission-mode check, for every permission mode. So deny still hold even under bypassPermissions. Na this make hook useful together with Claude Code auto mode and e permission settings, where prompts don reduce but hook still dey fire.
Make you understand wetin this be. Pattern matching for command string na guardrail against agent carelessness. E no be boundary against agent wey sabi bypass rules, because the same command fit dey written for form wey your grep no go ever see. Put hard rules for permission system and for the account wey the process dey run under.
Example 2: format and lint after every edit
PostToolUse wey get an Edit|Write matcher dey run after any file-editing tool. Save am as .claude/hooks/after-edit.sh:
#!/bin/bash
# Format the edited file, then report lint failures back to the model.
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
case "$FILE" in
*.py)
ruff format "$FILE" >/dev/null 2>&1
if ! ruff check "$FILE" >&2; then
exit 2
fi
;;
*.sh)
if ! shellcheck "$FILE" >&2; then
exit 2
fi
;;
esac
exit 0{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/after-edit.sh",
"timeout": 60
}
]
}
]
}
}Ask Claude make e add one function wey get bad indentation to one Python file, then open the file. E go come back formatted. Na this be your check say the hook run, because successful hook no show anything for the conversation.
The exit 2 for here no undo anything. PostToolUse dey fire after the tool don execute, so the edit dey disk either way. Wetin exit 2 give you be say the ruff check output reach the model as feedback, so e fit correct the error wey e just introduce instead of moving on. Na this be the difference between lint failure wey you discover when you commit and one wey the agent repair for the same turn.
Two matcher limits matter here. Edit|Write no dey see files wey shell command change, and Claude dey write files through Bash often enough make this gap real. For coverage on every call, match Bash too and make the script list changed files with git status --porcelain. For coverage once per turn, put the scan inside one Stop hook instead.
Example 3: tool call log for audit
Empty matcher for PostToolUse go run for every tool. If you send the record go system journal instead of file for home directory, the agent own shell no fit reach am:
{
"hooks": {
"PostToolUse": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "jq -c '{time: now|todate, session: .session_id, cwd: .cwd, tool: .tool_name, input: .tool_input}' | logger -t claude-code -p local0.info"
}
]
}
]
}
}Use journalctl -t claude-code -o cat | tail -n 5 read am back. You suppose see one JSON line for each tool call, with the newest one last. If nothing show, e mean the hook no run. The troubleshooting section below explain how to check am.
Add the same block under PostToolUseFailure to capture calls wey fail, because PostToolUse only run when command succeed. Failed command usually na the one wey matter pass. The reason for using logger instead of appending to file for your home directory na ownership: hook dey run as the same user as the agent shell. So anything wey that user fit append to, that user fit also truncate. systemd-journald dey write the journal with its own account.
How long hook fit run
The data behind this chart
[
{
"label": "command, http or mcp_tool hook",
"default_timeout_seconds": 600
},
{
"label": "agent hook",
"default_timeout_seconds": 60
},
{
"label": "prompt hook",
"default_timeout_seconds": 30
},
{
"label": "command hook on UserPromptSubmit",
"default_timeout_seconds": 30
},
{
"label": "command hook on MessageDisplay",
"default_timeout_seconds": 10
},
{
"label": "any hook on SessionEnd",
"default_timeout_seconds": 1.5
}
]A command hook get 600 seconds by default, wey be ten minutes. Some events fit cut this time down well well. SessionEnd hooks share 1.5 seconds budget among all of dem, so end-of-session cleanup must quick, though if you set longer timeout for the hook, e go raise that shared budget to match, up to 60 seconds.
If hook reach e timeout, system go cancel am and e no go make any decision. For PreToolUse guardrail, this mean e no go block anything: the tool call go continue into the normal permission flow. So, make guardrail scripts small. For slow work wey nobody dey wait for, like sending log somewhere, set "async": true and hook go run for background without delaying the tool call.
Hooks, rules files, skills and MCP servers
People dey confuse four things because all of dem fit change wetin agent dey do. But na only one of dem no be suggestion.
A rules file (CLAUDE.md, or file wey dey under .claude/rules/) na text wey model load into e context. E dey shape behaviour, but e no enforce anything. If long conversation, big diff, and fresh user request dey compete with am, one line from the file fit lose relevance. Na this ordinary mechanism dey make agents ignore the instructions wey you write down.
A skill na folder wey contain instructions and scripts wey model load when e judge say the skill relevant. That judgement na the main purpose of skill, and na there the limit dey too: model still dey decide. You fit see both sides for skill like Ponytail, wey dey push agent towards the smallest change wey go work, because e dey shape how model approach complete task in a way no hook fit do, and na only when model choose to load am.
An MCP (model context protocol) server dey give model new tools wey e fit call. E dey expand wetin agent fit reach. E no make agent use anything, and e na separate process wey you must operate. That one na work by itself: see running MCP servers on a VPS.
Hook na the only one among the four wey runs without model choosing am. Use rules file for preference, and use skill for procedure wey model suppose follow when e apply. Use hook for step wey must happen every time, or for thing wey must never happen. The deeper comparison, including when skill better pass rules file, dey for the skills, MCP and rules files comparison.
Plugin na packaging, no be fifth mechanism. E dey bundle hooks together with skills into one installable unit. Na so team fit ship the same guardrail to every machine: see how Claude Code plugins work.
Security decision wey you make for shared VPS
A hook na code wey the agent trigger, and e dey run as the user wey start Claude Code. E inherit that user's environment and file permissions. For laptop, na workflow question. For VPS wey agent dey run unattended, na security question with four practical parts.
A hook for repository na code wey you no write. .claude/settings.json dey committed, so if you clone repository and start session inside am, e fit register hooks wey come with the repository. Claude Code put project hooks behind workspace trust dialog for that folder. This mean say accepting trust na the moment wey you decide to run dem. Read the hooks block first.
A hook dey see the complete tool input. An audit hook wey log tool_input go write every argument of every command into file, including any token wey happen to dey for command line. That log then need the same protection wey the secret get. This na part of the bigger problem of keeping secrets away from AI agent.
A hook fit write enter model's context. Anything wey SessionStart or UserPromptSubmit hook print to stdout dey added to the conversation. A hook wey pipe text from outside, issue tracker, or log file dey hand untrusted text to the model as if na you type am yourself. A hook wey forward note from another Claude Code session for the same VPS dey do the same thing, and output from one agent no get more reason for trust than output from the issue tracker. Treat that stdout as input, no be output.
Privilege na the real control. Run the agent as dedicated unprivileged user with only the sudo rules wey e need. A PreToolUse deny dey useful, and e work as best effort by design: the reference talk the same thing about if filter and tell you to use permission system when you need hard deny. The permission rules and the account wey the process run under na the parts wey hold when pressure come.
One property dey hold for every configuration. PreToolUse hooks fire before permission-mode check for every permission mode, so hook wey return deny go block the tool even under bypassPermissions. Hooks fit make wetin permission rules allow more restrictive. Dem no fit make am less restrictive.
Wetin make my hook no dey fire?
Follow dem steps for this order. Each step tell you the symptom wey you go actually see.
- Run
/hooksand check say the hook dey show under the event wey you expect. If hook no dey for the menu, e usually mean say the settings file get JSON syntax error, because trailing commas and comments no dey allowed, or the file no dey inside one of the six locations wey dem list above. - Compare the matcher with the tool name exactly. Matchers dey case sensitive, so
"bash"no go ever matchBashtool. - Run the script by hand with sample input, like example 1 above. If exit code no be the one wey you expect, na bug for your script, and Claude Code go report am as hook error instead of decision.
- Notice wey read
jq: command not foundmean sayjqno dey for that machine. If you getcommand not foundfor your own script, e mean say the path no resolve, so use${CLAUDE_PROJECT_DIR}or absolute path. If the script no run at all, e probably no get execute permission. - The hook print valid JSON but nothing happen. Shell-form hook dey run through
sh -c, and if your shell profile print banner, dem go add that banner before your JSON. Stdout no longer start with{, so Claude Code go read the whole output as plain text and ignore the decision. When exit 0 happen, nothing dey report anywhere except the debug log. Put anyechoinside your profile so e only run for interactive shells. - If problem still dey, start the session with
claude --debug-file /tmp/claude.logand runtail -f /tmp/claude.logfor second terminal. The debug log record which hooks match, the exit code wey each one return, and everything wey dem write to stdout and stderr.
FAQ
Difference between Claude Code hook and CLAUDE.md instruction na wetin?
A CLAUDE.md instruction na text wey dey inside model context, so e dey compete for attention with conversation and current request, and model fit weigh am against dem. Hook na shell command wey Claude Code dey run for one fixed point of im lifecycle, so e go execute every time im event happen, no matter wetin model decide. Use instruction for preference. Use hook for step wey must always happen or action wey must never happen.
How I fit stop Claude Code from running one specific shell command?
Register PreToolUse hook with Bash matcher wey reads command from .tool_input.command, writes reason to stderr, then exits 2. Claude Code go cancel the call and show model your reason. This one happen before permission-mode check, so the deny still hold even for bypassPermissions mode. Pattern matching for command string na guardrail, no be security boundary, because person fit write the same command for format wey pattern no catch. So back am with permission rules and unprivileged account.
My hook dey print valid JSON but nothing dey happen. Why?
The commonest cause na your shell profile. Hook wey no get args field dey run through sh -c, and some profiles dey print banner for every shell. This banner go enter stdout before your JSON. Because output no longer start with {, Claude Code go treat everything as plain text and ignore the decision. If process exits 0, nothing go show for transcript at all. Put interactive-shell test around any echo for your profile. Then confirm the fix by reading debug log from claude --debug-file /tmp/claude.log.
E safe to run Claude Code hooks for shared server?
Hooks dey run as the user wey start Claude Code, with that user file permissions. So hook fit do anything wey that account fit do. Two habits fit cover most of the risk: run the agent as dedicated unprivileged account with narrow sudo policy, and read the hooks block of any repository before you accept workspace trust dialog, because project hooks ship inside .claude/settings.json. Set "disableAllHooks": true for your settings file when you no want any of dem to run.