Hooks are automatic reactions to events in a Claude Code session. When something happens -- Claude is about to run a command, a file is written, a session starts -- a hook can run a script in response. Think of them as "if this, then that" rules for your workspace. Claude doesn't control hooks and isn't aware they're running. They operate at the system level, outside the conversation.
That separation is what makes hooks useful for infrastructure tasks: audit logging, safety guardrails, automatic backups, and session continuity. A skill is something you invoke intentionally. A hook is something that happens regardless of what you or Claude are doing.
Hooks vs skills
Skills are invoked intentionally by you or Claude. Hooks are triggered automatically by the system -- Claude doesn't know they're running. Use skills for deliberate workflows; use hooks for infrastructure behaviour that should always happen regardless of what Claude is doing.
All lifecycle events
| Event | When it fires | Blocking? | Common use |
|---|---|---|---|
SessionStart | Session opens (once) | No | Log session start, load env state |
SessionEnd | Session closes (once) | No | Final cleanup, archive session log |
InstructionsLoaded | CLAUDE.md and rules loaded (once) | No | Validate config, inject env-specific context |
ToolUseStart | Before each tool call | Yes | Validate paths, block restricted operations |
ToolUseEnd | After each tool call completes | Yes | Log file writes, trigger downstream actions |
PreCompact | Before context compression | No | Write checkpoint -- preserve where you are |
WorktreeCreate | Git worktree created for an agent | No | Log agent isolation, copy env files |
WorktreeRemove | Git worktree removed | No | Clean up temp files, notify on completion |
LoopStart | A /loop skill begins | No | Log recurring task start |
LoopIteration | Each iteration of a loop | No | Monitor, alert on anomalies |
LoopEnd | Loop completes or is stopped | No | Send summary notification |
Blocking vs non-blocking
Blocking hooks (ToolUseStart, ToolUseEnd) make Claude Code wait for the hook to complete before proceeding. They can return a JSON decision that allows or blocks the operation. All other events are non-blocking -- the session continues while the hook runs asynchronously.
A blocking hook returns JSON on stdout:
# Allow the operation
{"decision": "allow"}
# Block the operation with a reason
{"decision": "block", "reason": "Write to /etc is not permitted"}
# Override tool output (PostToolUse only)
{"decision": "allow", "output": "patched tool result"}
Exit code 0 = hook succeeded. Any non-zero exit code = hook failed. For blocking hooks, failure causes Claude Code to block the operation and report the error to the session.
What shell scripting means for hooks
Most hooks are written as shell scripts -- small text files that run commands on your computer, one line at a time. Shell scripts are the standard way to automate system-level tasks on macOS and Linux. You do not need to know how to write them from scratch: you can describe what you want a hook to do and ask Claude to write the script for you. The scripts that ship with the AI Business OS were written this way.
If you look inside a hook script and see something that looks unfamiliar, the most common elements are: reading input with cat (which collects the event data Claude sends), writing output with echo (which sends a response back), and checking conditions with if statements (which decide whether to allow or block something). Everything else is variations on those three ideas.
Hook input -- JSON on stdin
Every hook receives a JSON object on stdin describing the event. For tool hooks, this includes the tool name, inputs, and result (PostToolUse):
{
"event": "ToolUseStart",
"tool": "Bash",
"input": {
"command": "rm -rf /tmp/old-files"
},
"session_id": "sess_abc123"
}
Your hook script reads this with cat or jq to extract what it needs. jq is a small tool for reading structured data files -- it lets you pull out a specific value (like the tool name, or the file path being written) from the JSON that Claude sends to the hook:
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool')
COMMAND=$(echo "$INPUT" | jq -r '.input.command // empty')
# Block rm -rf anywhere
if [[ "$COMMAND" == *"rm -rf"* ]]; then
echo '{"decision":"block","reason":"rm -rf requires explicit confirmation"}'
exit 0
fi
echo '{"decision":"allow"}'
Configuration
Hooks are registered in .claude/settings.json under the hooks key. Each entry specifies the event, an optional matcher, and the handler.
{
"hooks": {
"PreCompact": [
{
"command": ".claude/hooks/precompact-checkpoint.sh"
}
],
"ToolUseEnd": [
{
"matcher": "Write",
"command": ".claude/hooks/log-file-writes.sh"
}
],
"ToolUseStart": [
{
"matcher": "Bash",
"command": ".claude/hooks/validate-bash.sh"
}
]
}
}
The matcher field filters which tool calls trigger the hook. It supports exact tool names ("Write"), glob patterns ("Write(./.env*)"), and regex patterns. Omit the matcher to fire on every tool call for that event.
Handler types
- Shell scripts -- most common; any executable bash/zsh script, receives JSON on stdin
- HTTP endpoints -- Claude Code sends a POST request with the event JSON to a local or remote URL
- LLM handlers -- Claude Code makes a model call to evaluate the event, asking something like "should this bash command be allowed?" and using the AI's response as the decision. Slower than a script, but capable of nuanced judgement that a simple if-statement can't make -- for example, deciding whether a file path looks suspicious rather than matching against a fixed list.
Example: checkpoint before compaction
This hook appends a timestamped breadcrumb to a checkpoint file whenever the context window is about to be compressed -- marking the exact point where session memory may be lost:
#!/bin/bash
# .claude/hooks/precompact-checkpoint.sh
CHECKPOINT_DIR="$HOME/Documents/Agent Outputs/general"
mkdir -p "$CHECKPOINT_DIR"
FILE="$CHECKPOINT_DIR/$(date +%Y-%m-%d)-session-checkpoint.md"
{
echo ""
echo "---"
echo "## Compaction Event — $(date +%H:%M)"
echo "**Context compacted.** Resume from this point forward."
} >> "$FILE"
Example: block writes to sensitive paths
#!/bin/bash
# .claude/hooks/protect-sensitive-paths.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool')
if [[ "$TOOL" == "Write" ]]; then
PATH_ARG=$(echo "$INPUT" | jq -r '.input.file_path // empty')
if [[ "$PATH_ARG" == *"/.env"* || "$PATH_ARG" == *"/secrets"* ]]; then
echo '{"decision":"block","reason":"Direct writes to .env and secrets paths are blocked"}'
exit 0
fi
fi
echo '{"decision":"allow"}'
Testing hooks
Before relying on a hook in production, verify it works as expected. The simplest approach is to trigger the event manually and check the output.
- Run the script directly in terminal. Pass a sample JSON payload via echo:
echo '{"event":"ToolUseStart","tool":"Bash","input":{"command":"rm -rf /tmp"}}' | bash .claude/hooks/validate-bash.sh. The script should print its JSON decision to the terminal. - Check exit codes. A decision of "allow" with exit code 0 means the hook passed. A "block" decision with exit code 0 means it blocked correctly. A non-zero exit code means the script itself errored.
- Test in a live session. Perform the action the hook is watching for and confirm the expected behaviour. For a logging hook, verify the log file was written. For a blocking hook, confirm the operation was stopped.
- Check the log. Claude Code logs hook execution. On macOS:
~/Library/Logs/Claude/. If a hook isn't firing, this is the first place to look.
Common hooks for business use
You do not need to build hooks from scratch. These patterns cover the most common business uses -- ask Claude to write the script for any of them.
- Change log. A
ToolUseEndhook on the Write tool that appends every file write to a daily log file. Useful for reviewing what changed in a session, or recovering from an unintended edit. - Completion notification. A
SessionEndorLoopEndhook that sends a desktop notification (macOS:osascript -e 'display notification...') when a long-running task finishes. - Auto-backup. A
ToolUseEndhook on the Edit tool that runs a git commit after each file is modified, creating a granular history you can roll back through. - Path protection. A
ToolUseStarthook on the Bash tool that blocks commands matching patterns you want to require confirmation for --rm -rf, writes to system directories, or commands that touch client folders you've marked as protected.
Good uses for hooks
- Session checkpointing before context compression (
PreCompact) - Audit log of all file writes for recovery or review (
ToolUseEnd+ Write matcher) - Block writes to
.env, system directories, or out-of-scope paths (ToolUseStart) - Send a notification when a long-running task finishes (
SessionEndorLoopEnd) - Auto-commit after a successful code change (
ToolUseEnd+ Edit matcher) - Copy
.envinto a new agent worktree on creation (WorktreeCreate)
