Hooks
Introduction
Hooks are lifecycle event handlers that run in response to things Claude does. They let you enforce rules that go beyond what CLAUDE.md instructions or settings.json permissions can do — blocking specific commands, validating tool inputs, injecting context, and reacting to session events.
If you haven’t read the earlier guides: Installing Claude Code, Using the Claude Code TUI, CLAUDE.md and Settings, Skills, Agents, and MCP Servers.
How Hooks Work
A hook is a definition in your settings.json that says “when this event happens, run this thing.” Claude Code fires events at key points in its lifecycle — before using a tool, after using a tool, when a session starts, when Claude tries to stop, and more. Your hooks listen for these events and can inspect, modify, or block what’s happening.
Hooks are defined in the same settings.json files we covered in the settings guide (user, project, local, enterprise). They follow the same cascade — all matching hooks run in parallel, and deny takes precedence.
Hook Types
There are four types of hooks, each suited to different tasks:
Command Hooks
The most common type. Runs a shell command and uses the exit code to determine the outcome.
{
"type": "command",
"command": "bash -c 'echo \"$TOOL_INPUT\" | jq -r .command | grep -qv \"rm -rf\"'"
}Exit codes:
- 0 — Success. If stdout contains JSON, it’s parsed for decisions.
- 2 — Blocking error. The stderr message is fed back to Claude as feedback.
- Other — Non-blocking error. Logged but doesn’t stop anything.
HTTP Hooks
Sends a POST request to an endpoint. Useful for integrating with external services like logging, alerting, or approval systems.
{
"type": "http",
"url": "https://your-service.example.com/hook",
"allowedEnvVars": ["HOOK_AUTH_TOKEN"]
}Prompt Hooks
A single-turn LLM evaluation. Claude assesses the situation and returns a JSON verdict. Useful for nuanced decisions that can’t be captured in a simple pattern match.
{
"type": "prompt",
"prompt": "Check if this bash command could delete or overwrite important files. Return {\"ok\": true} if safe or {\"ok\": false, \"reason\": \"...\"} if dangerous."
}Agent Hooks
A multi-turn sub-agent with access to Read, Grep, and Glob tools. Like prompt hooks but can investigate the codebase before making a decision. Useful for complex validations.
{
"type": "agent",
"prompt": "Review this file edit to ensure it follows our coding conventions. Check the project's CLAUDE.md for conventions. Return {\"ok\": true} if compliant or {\"ok\": false, \"reason\": \"...\"} with specific issues."
}Events
Hooks fire on specific events. Here are the most useful ones:
| Event | When it fires | Common use |
|---|---|---|
PreToolUse | Before Claude uses a tool | Block dangerous commands, validate inputs |
PostToolUse | After a tool runs successfully | Log actions, validate outputs |
PostToolUseFailure | After a tool fails | Custom error handling |
UserPromptSubmit | When you send a message | Inject context, validate prompts |
Stop | When Claude tries to finish | Verify task completion |
SubagentStop | When a sub-agent finishes | Verify sub-task completion |
SessionStart | When a session begins | Set up environment, inject context |
SessionEnd | When a session ends | Cleanup, logging |
Notification | When Claude sends a notification | Custom notification routing |
PreCompact | Before context compaction | Preserve important information |
Matchers
Most events support matchers to narrow when a hook fires. The most common is filtering by tool name:
{
"hooks": {
"PreToolUse": [
{
"matcher": { "toolName": "Bash" },
"type": "command",
"command": "bash -c 'validate-command.sh'"
}
]
}
}This hook only fires when Claude is about to use the Bash tool, not for Read, Write, or any other tool.
Configuring Hooks
Hooks go in the hooks section of your settings.json:
.claude/settings.json
{
"permissions": {
"allow": ["Read(*)", "Bash(git status)"],
"deny": ["Bash(rm -rf *)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": { "toolName": "Bash" },
"type": "command",
"command": "bash -c 'echo \"$TOOL_INPUT\" | python3 validate-bash.py'"
}
],
"Stop": [
{
"type": "prompt",
"prompt": "Check if all requested tasks were completed. Return {\"ok\": true} if done or {\"ok\": false, \"reason\": \"...\"} if work remains."
}
]
}
}Multiple hooks can listen to the same event — they all run in parallel. If any hook blocks, the action is blocked.
Hook Outputs
Hooks communicate back to Claude Code through their output. Command hooks return JSON on stdout; prompt and agent hooks return structured decisions directly.
The key output fields:
| Field | Effect |
|---|---|
continue | false stops processing (default: true) |
stopReason | Message shown when stopping |
suppressOutput | Hide the tool’s output from Claude |
systemMessage | Warning message shown to the user |
For PreToolUse hooks specifically, there’s a hookSpecificOutput with:
| Field | Effect |
|---|---|
permissionDecision | allow, deny, or ask |
permissionDecisionReason | Explanation for the decision |
updatedInput | Modified tool input (lets you rewrite commands) |
Practical Examples
Blocking Dangerous Commands
A PreToolUse hook that blocks destructive bash commands:
.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": { "toolName": "Bash" },
"type": "command",
"command": "bash -c 'CMD=$(echo \"$TOOL_INPUT\" | jq -r .command); echo \"$CMD\" | grep -qE \"rm -rf|DROP TABLE|truncate|format|mkfs\" && echo \"{\\\"hookSpecificOutput\\\": {\\\"permissionDecision\\\": \\\"deny\\\", \\\"permissionDecisionReason\\\": \\\"Destructive command blocked by hook\\\"}}\" || true'"
}
]
}
}This reads the command Claude is about to run, checks it against a list of destructive patterns, and denies it if there’s a match. Unlike a settings.json deny rule which blocks based on a simple pattern, a hook can do more sophisticated validation — checking arguments, looking at the current directory, or calling external scripts.
Protecting Sensitive Files
A hook that prevents Claude from writing to sensitive files:
.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": { "toolName": "Write" },
"type": "command",
"command": "bash -c 'FILE=$(echo \"$TOOL_INPUT\" | jq -r .file_path); echo \"$FILE\" | grep -qE \"\\.(env|pem|key)$|secrets/|credentials\" && { echo \"Blocked: write to sensitive file $FILE\" >&2; exit 2; } || true'"
}
]
}
}Exit code 2 blocks the action and feeds the stderr message back to Claude, so it knows why the write was denied and can adjust its approach.
Redirecting Commands to Better Alternatives
A hook that nudges Claude to use preferred tools:
.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": { "toolName": "Bash" },
"type": "command",
"command": "bash -c 'CMD=$(echo \"$TOOL_INPUT\" | jq -r .command); if echo \"$CMD\" | grep -q \"^grep \"; then echo \"Use rg instead of grep for better performance\" >&2; exit 2; fi'"
}
]
}
}When Claude tries to run grep, the hook blocks it with a message suggesting rg instead. Claude reads the feedback and adjusts.
Verifying Task Completion
A Stop hook that uses an LLM prompt to verify Claude actually finished the work:
.claude/settings.json
{
"hooks": {
"Stop": [
{
"type": "prompt",
"prompt": "Review the conversation and check if all tasks the user requested have been completed. If any requested work is still incomplete, return {\"ok\": false, \"reason\": \"Still need to: [list remaining tasks]\"}. If everything is done, return {\"ok\": true}."
}
]
}
}This prevents Claude from stopping prematurely — if the prompt hook decides work remains, Claude continues.
Session Startup Context
A SessionStart hook that injects useful context at the beginning of every session:
.claude/settings.json
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "bash -c 'echo Current branch: $(git branch --show-current); echo Last commit: $(git log --oneline -1); echo Uncommitted changes: $(git status --porcelain | wc -l) files'"
}
]
}
}Every time you start Claude Code, this hook prints the current git state so Claude has immediate context about where things are.
Hooks vs Settings Permissions
You might wonder when to use hooks versus the allow/deny rules in settings.json. Here’s the distinction:
- Settings permissions are simple pattern matching. They’re fast, reliable, and good for broad rules like “never allow
rm -rf” or “always allowgit status.” - Hooks can run arbitrary logic. They can inspect the full command, check the current state of the project, call external services, or use an LLM to make nuanced decisions.
Use settings permissions as your first line of defence — they’re simpler and can’t be worked around. Use hooks for anything that needs more context or logic than a pattern match can provide.
Wrapping Up
Hooks give you programmable control over Claude Code’s behaviour at every stage of its lifecycle. Start simple — a PreToolUse hook to block a command you’re worried about, or a SessionStart hook to inject useful context. You can always add more as you discover patterns.
The most effective setups combine all three layers: settings.json permissions for broad rules, CLAUDE.md instructions for guidance, and hooks for enforcement and automation.
The next guide covers plugins and marketplaces — how to package everything we’ve covered so far into distributable, reusable units and discover plugins built by others.