Hooks are shell commands that Claude Code fires automatically in response to its own actions. They're how you enforce formatting, inject external context, block dangerous operations, and build session-level automation — without interrupting your workflow to ask the agent to do it manually.
A hook is a shell command (or script path) that Claude Code runs at specific points in its execution lifecycle. Hooks run in your local shell environment, have access to the same environment variables as your terminal session, and can read information about the tool call that triggered them via stdin.
Hooks are configured in ~/.claude/settings.json — your global Claude Code configuration file. They apply to every project you work in, unless you override them at the project level.
There are four hook event types, each firing at a different point in Claude's execution:
Fires before Claude executes any tool call. Can inspect and block the call. Use for safety checks, validation, and semgrep-style pre-flight analysis.
Fires after a tool call completes. Cannot block (the action already happened), but can trigger follow-up automation: formatting, linting, logging.
Fires when you submit a prompt, before Claude processes it. Use to inject additional context — RAG results, environment state, session memory.
Fires when the session ends or Claude finishes responding. Use for session memory persistence, cleanup, final state saves.
Hooks live in the hooks array inside ~/.claude/settings.json. Each hook object has these fields:
| Field | Type | Purpose |
|---|---|---|
hookEvent |
string | Which event triggers this hook: PreToolUse, PostToolUse, UserPromptSubmit, Stop |
matcher |
string (optional) | Tool name filter. If set, hook only fires for matching tool calls (e.g., "Write", "Bash"). Omit to match all tools. |
command |
string | The shell command to run. Can be inline or a path to a script. |
timeout |
number (optional) | Milliseconds before the hook is killed. Default is 30000 (30s). Set lower for fast checks, higher for RAG queries. |
{
"hooks": [
{
"hookEvent": "PostToolUse",
"matcher": "Write",
"command": "~/.claude/hooks/auto-format.sh",
"timeout": 10000
},
{
"hookEvent": "PreToolUse",
"matcher": "Bash",
"command": "python3 ~/.claude/hooks/security_reminder_hook.py",
"timeout": 5000
}
]
}
A hook's exit code determines what Claude Code does next. This is how you enforce rules without building complex middleware:
| Exit Code | Meaning | Effect |
|---|---|---|
| 0 | Allow / OK | Proceed normally. For PreToolUse, the tool call executes. |
| 1 | Block / Error | For PreToolUse: block the tool call entirely. Claude is informed and can try a different approach. For PostToolUse: logged as an error but cannot undo the action. |
| 2 | Non-blocking warn | Proceed but surface a warning to Claude and the user. Useful for "this looks risky but I won't stop it" scenarios. |
import sys, json; data = json.load(sys.stdin).
Fires every time Claude writes a .py file, runs ruff then black automatically. No more "please format this" prompts.
#!/usr/bin/env bash
# ~/.claude/hooks/auto-format.sh
# Reads the written file path from stdin JSON, formats if .py
FILE=$(python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('file_path', ''))
")
if [[ "$FILE" == *.py ]]; then
ruff check --fix "$FILE" 2>/dev/null
black --quiet "$FILE" 2>/dev/null
fi
exit 0 # always allow — this is post-write, nothing to block
Settings entry:
{
"hookEvent": "PostToolUse",
"matcher": "Write",
"command": "~/.claude/hooks/auto-format.sh",
"timeout": 15000
}
Fires before every Bash tool call. Checks for patterns that should trigger a pause — destructive commands, production targets, force pushes.
#!/usr/bin/env python3
# ~/.claude/hooks/security_reminder_hook.py
import sys, json, re
data = json.load(sys.stdin)
command = data.get("command", "")
DANGER_PATTERNS = [
(r"rm\s+-rf\s+/", "rm -rf from root is extremely dangerous"),
(r"git push.*--force.*main", "force push to main is blocked"),
(r"git push.*--force.*master", "force push to master is blocked"),
(r"drop\s+table", "SQL DROP TABLE — confirm this is intentional"),
(r":\s*>\s*/etc/", "overwriting system files"),
]
for pattern, message in DANGER_PATTERNS:
if re.search(pattern, command, re.IGNORECASE):
print(f"BLOCKED: {message}", flush=True)
sys.exit(1) # block the command
# Warn (but allow) on staging/prod keywords
WARN_PATTERNS = ["production", "prod-", "staging", "--force"]
for p in WARN_PATTERNS:
if p in command.lower():
print(f"WARNING: command targets a sensitive environment: {p}")
sys.exit(2) # warn but allow
sys.exit(0)
Before Claude sees your prompt, this hook queries the local RAG system and prepends relevant knowledge chunks. Claude gets grounded context without you having to paste it manually.
#!/usr/bin/env bash
# ~/.claude/hooks/rag-inject.sh
# Queries RAG and prepends results to the prompt via stdout
PROMPT=$(python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('prompt', ''))
")
# Query the local RAG system (Kernel agent — code-focused, 0.2 temp)
RAG_RESULT=$(curl -s -X POST http://your-rag-server:9990/query \
-H "Content-Type: application/json" \
-d "{\"query\": \"$PROMPT\", \"agent\": \"kernel\", \"k\": 3}" \
--max-time 5 2>/dev/null)
if [ -n "$RAG_RESULT" ]; then
CONTEXT=$(echo "$RAG_RESULT" | python3 -c "
import sys, json
data = json.load(sys.stdin)
chunks = data.get('chunks', [])
print('\n'.join(f'- {c[\"text\"]}' for c in chunks[:3]))
")
# Write augmented prompt to stdout — Claude Code reads this
echo "=== RAG Context (auto-injected) ===
$CONTEXT
=== End RAG Context ===
$PROMPT"
fi
exit 0
When the session ends, write a timestamped summary of what happened to a session log file. Useful for building long-term memory outside the context window.
#!/usr/bin/env bash
# ~/.claude/hooks/save-session.sh
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
LOG_DIR=~/.claude/session-logs
mkdir -p "$LOG_DIR"
# The Stop event passes session summary data via stdin
python3 -c "
import sys, json
data = json.load(sys.stdin)
summary = data.get('summary', 'No summary available')
print(summary)
" >> "$LOG_DIR/session-$TIMESTAMP.md"
exit 0
Run semgrep rules against code before Claude writes it to disk. Catches security patterns at the point of creation, not after.
#!/usr/bin/env bash
# ~/.claude/hooks/semgrep-check.sh
# Runs semgrep on content before it's written
FILE=$(python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('file_path', ''))
")
CONTENT=$(python3 -c "
import sys, json
data = json.load(sys.stdin)
print(data.get('content', ''))
")
# Only check source files, skip lockfiles and generated code
if [[ "$FILE" =~ \.(js|ts|py|rs|go)$ ]]; then
TMPFILE=$(mktemp --suffix="${FILE##*.}")
echo "$CONTENT" > "$TMPFILE"
semgrep --config=auto "$TMPFILE" --quiet 2>/dev/null
EXIT=$?
rm -f "$TMPFILE"
if [ $EXIT -ne 0 ]; then
echo "Semgrep found issues in $FILE — review before writing"
exit 2 # warn but allow (change to 1 to block)
fi
fi
exit 0
{
"hooks": [
{
"hookEvent": "PostToolUse",
"matcher": "Write",
"command": "~/.claude/hooks/auto-format.sh",
"timeout": 15000
},
{
"hookEvent": "PreToolUse",
"matcher": "Bash",
"command": "python3 ~/.claude/hooks/security_reminder_hook.py",
"timeout": 5000
},
{
"hookEvent": "UserPromptSubmit",
"command": "~/.claude/hooks/rag-inject.sh",
"timeout": 8000
},
{
"hookEvent": "Stop",
"command": "~/.claude/hooks/save-session.sh",
"timeout": 10000
},
{
"hookEvent": "PreToolUse",
"matcher": "Write",
"command": "~/.claude/hooks/semgrep-check.sh",
"timeout": 20000
}
]
}
Use PreToolUse on Bash to intercept destructive operations before they execute:
| Pattern to block | Why | Exit code |
|---|---|---|
rm -rf /, rm -rf ~ |
Data destruction | 1 (hard block) |
git push --force main/master |
Rewrites shared history | 1 (hard block) |
DROP TABLE without WHERE |
Database destruction | 1 (hard block) |
curl ... | bash |
Arbitrary remote code execution | 2 (warn and allow with audit) |
Commands containing production |
Live environment — extra caution | 2 (warn) |
A PreToolUse:Write hook can scan file content before writing for patterns that look like API keys, tokens, or credentials:
#!/usr/bin/env python3
import sys, json, re
data = json.load(sys.stdin)
content = data.get("content", "")
file_path = data.get("file_path", "")
SECRET_PATTERNS = [
r"sk-[a-zA-Z0-9]{48}", # OpenAI key
r"ghp_[a-zA-Z0-9]{36}", # GitHub PAT
r"AKIA[0-9A-Z]{16}", # AWS access key
r"-----BEGIN (RSA|EC) PRIVATE KEY", # Private key
]
for pattern in SECRET_PATTERNS:
if re.search(pattern, content):
print(f"BLOCKED: Possible secret detected in {file_path}")
sys.exit(1)
sys.exit(0)
If you're doing a focused work session, a Stop hook can automatically create a WIP commit so nothing gets lost:
#!/usr/bin/env bash
# Only auto-commit if there are staged or modified files
if ! git diff --quiet || ! git diff --staged --quiet; then
git add -p # interactive staging is better, but for automation:
git add $(git diff --name-only)
git commit -m "wip: auto-commit at session end $(date +%Y-%m-%d_%H:%M)"
fi
exit 0
A Stop hook can send a desktop notification or message when Claude finishes a long-running task:
#!/usr/bin/env bash
# Send desktop notification
notify-send "Claude Code" "Session complete" --icon=claude 2>/dev/null
# Or send to Telegram
# curl -s "https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage" \
# -d "chat_id=$CHAT_ID&text=Claude Code session complete"
exit 0
source ~/.bashrc or source ~/.zshrc.
PATH. Use absolute paths for commands in hooks (/usr/bin/python3, /home/youruser/.local/bin/ruff) or explicitly set PATH at the top of your hook script.
echo '{"command": "rm -rf /"}' | python3 ~/.claude/hooks/security_reminder_hook.py; echo "Exit: $?"settings.json with a generous timeout| Use case | Event | Matcher | Exit for block |
|---|---|---|---|
| Auto-format code | PostToolUse | Write | N/A (post-write) |
| Block dangerous shell commands | PreToolUse | Bash | 1 |
| Inject RAG context | UserPromptSubmit | (none) | N/A |
| Save session memory | Stop | (none) | N/A |
| Semgrep pre-flight | PreToolUse | Write | 1 or 2 |
| Prevent secret commits | PreToolUse | Write | 1 |
| Warn on prod operations | PreToolUse | Bash | 2 |
See also: CLAUDE.md Guide · Writing Skills · Knowledge Graphs