一键导入
10-hooks
Create pre- and post-command hooks that run automatically when Claude Code executes tools, giving you guardrails and automation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Create pre- and post-command hooks that run automatically when Claude Code executes tools, giving you guardrails and automation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Install Claude Code and configure Google Cloud Vertex AI as the backend provider.
Learn how to write a CLAUDE.md file that shapes Claude Code's behavior in any project.
Teach Claude Code to remember across sessions using the Memory MCP knowledge graph.
Use Git operations from inside Claude Code via the Git MCP server.
Connect Claude Code to Jira via the Atlassian Rovo MCP server.
Use browser automation from inside Claude Code via the Playwright MCP server.
| name | 10-hooks |
| description | Create pre- and post-command hooks that run automatically when Claude Code executes tools, giving you guardrails and automation. |
Estimated time: 15 minutes Prerequisites: Module 01 (Claude Code installed and working)
Create pre- and post-command hooks that run automatically when Claude Code executes tools, giving you guardrails and automation.
If you already understand hooks and just want a working guardrail:
mkdir -p .claude/hooks.claude/hooks/guard-git.sh:
#!/bin/bash
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
[ "$TOOL_NAME" != "Bash" ] && exit 0
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
echo "$COMMAND" | grep -qE 'git\s+push\s+.*--force|git\s+reset\s+--hard|git\s+clean\s+-fd' && { echo "BLOCKED: Destructive git command"; exit 2; }
exit 0
chmod +x .claude/hooks/guard-git.sh.claude/settings.local.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/guard-git.sh" }] }
]
}
}
Verify: ask Claude to run git push --force — the hook should block it.
Skip to the Challenge for hands-on practice.
Print this once at the start:
You're learning Claude Code hooks.
This takes about 15 minutes.
We'll cover:
1. What hooks are and when they fire
2. Writing a pre-tool hook (guardrail)
3. Writing a post-tool hook (automation)
4. Hook configuration in settings.json
You'll need:
- Claude Code installed and working (Module 01)
On module start, write a progress marker:
mkdir -p ~/.claude/courseware-progress && date -u +%Y-%m-%dT%H:%M:%SZ > ~/.claude/courseware-progress/10.started
Audit current state before doing anything. Each check prints EXISTS or MISSING.
# Check 1 — Claude Code installed?
command -v claude &>/dev/null && echo "EXISTS: Claude Code" || echo "MISSING: Claude Code — run /learn-01-vertex-setup first"
# Check 2 — Existing hooks configured?
if [ -f "$HOME/.claude/settings.json" ]; then
HOOK_COUNT=$(python3 -c "
import json
d = json.load(open('$HOME/.claude/settings.json'))
hooks = d.get('hooks', {})
count = 0
for event in hooks:
if isinstance(hooks[event], list):
count += len(hooks[event])
print(count)
" 2>/dev/null)
if [ "$HOOK_COUNT" -gt 0 ] 2>/dev/null; then
echo "EXISTS: $HOOK_COUNT hook(s) already configured"
else
echo "INFO: No hooks configured yet (expected — we'll create some)"
fi
else
echo "INFO: No settings.json found — will create hooks in project settings"
fi
# Check 3 — Project .claude/settings.local.json exists?
if [ -f ".claude/settings.local.json" ]; then
echo "EXISTS: Project-local settings file"
else
echo "INFO: No project-local settings — hooks can go in user or project settings"
fi
Print a summary of what was found.
If Claude Code is MISSING, stop:
Claude Code is not installed. Complete Module 01 first:
/learn-01-vertex-setup
Hooks are shell commands that Claude Code runs automatically before or after tool calls. They let you add guardrails, logging, and automation without modifying Claude's behavior directly.
Tell the user:
Claude Code hooks fire at specific events:
PreToolUse — runs BEFORE a tool is executed
Can block the action (exit non-zero = blocked)
PostToolUse — runs AFTER a tool completes
Gets the tool result, can log or react
Notification — runs when Claude sends a notification
(e.g., background task complete)
Stop — runs when Claude finishes a response
SubagentStop — runs when a subagent completes
Hook config goes in:
~/.claude/settings.json (global — all projects)
.claude/settings.json (project — this repo only)
.claude/settings.local.json (project-local — gitignored)
Each hook has:
- matcher: which tool to match (regex or exact)
- command: shell command to run
- timeout: max execution time (ms, default 60000)
This hook prevents destructive git operations by blocking force-push and hard-reset commands.
Tell the user:
Let's create a safety hook that blocks dangerous git commands.
This is a PreToolUse hook that inspects Bash tool calls and
blocks "git push --force" and "git reset --hard".
The hook script receives tool input as JSON on stdin. It reads
the command being run and checks for dangerous patterns.
Create the hook script:
mkdir -p .claude/hooks
cat > .claude/hooks/guard-git.sh << 'HOOKEOF'
#!/bin/bash
# PreToolUse hook: block destructive git commands
# Receives tool input as JSON on stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
if [ "$TOOL_NAME" != "Bash" ]; then
exit 0
fi
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
# Block dangerous patterns
if echo "$COMMAND" | grep -qE 'git\s+push\s+.*--force|git\s+reset\s+--hard|git\s+clean\s+-fd'; then
echo "BLOCKED: Destructive git command detected: $COMMAND"
echo "Use the standard git workflow instead."
exit 2
fi
exit 0
HOOKEOF
chmod +x .claude/hooks/guard-git.sh
echo "PASS: Guard hook created at .claude/hooks/guard-git.sh"
Now register it in settings. Tell the user:
Now we need to register this hook. I'll add it to the project's
local settings so it only applies here.
python3 << 'PYEOF'
import json, os
path = ".claude/settings.local.json"
try:
with open(path) as f:
settings = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
settings = {}
if "hooks" not in settings:
settings["hooks"] = {}
if "PreToolUse" not in settings["hooks"]:
settings["hooks"]["PreToolUse"] = []
# Check if hook already exists
existing = [h for h in settings["hooks"]["PreToolUse"] if "guard-git" in h.get("command", "")]
if not existing:
settings["hooks"]["PreToolUse"].append({
"matcher": "Bash",
"command": "bash .claude/hooks/guard-git.sh",
"timeout": 5000
})
with open(path, "w") as f:
json.dump(settings, f, indent=2)
print("PASS: PreToolUse hook registered in .claude/settings.local.json")
PYEOF
This hook logs all file modifications to a local audit trail.
Tell the user:
Now let's create a PostToolUse hook that logs every file
Claude Code modifies. This creates an audit trail you can
review later.
cat > .claude/hooks/log-edits.sh << 'HOOKEOF'
#!/bin/bash
# PostToolUse hook: log file modifications
# Receives tool result as JSON on stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
# Only log Edit and Write tools
if [ "$TOOL_NAME" != "Edit" ] && [ "$TOOL_NAME" != "Write" ]; then
exit 0
fi
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path','unknown'))" 2>/dev/null)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
mkdir -p .claude/logs
echo "$TIMESTAMP | $TOOL_NAME | $FILE_PATH" >> .claude/logs/edit-audit.log
exit 0
HOOKEOF
chmod +x .claude/hooks/log-edits.sh
echo "PASS: Logging hook created at .claude/hooks/log-edits.sh"
Register it:
python3 << 'PYEOF'
import json
path = ".claude/settings.local.json"
with open(path) as f:
settings = json.load(f)
if "PostToolUse" not in settings["hooks"]:
settings["hooks"]["PostToolUse"] = []
existing = [h for h in settings["hooks"]["PostToolUse"] if "log-edits" in h.get("command", "")]
if not existing:
settings["hooks"]["PostToolUse"].append({
"matcher": "(Edit|Write)",
"command": "bash .claude/hooks/log-edits.sh",
"timeout": 5000
})
with open(path, "w") as f:
json.dump(settings, f, indent=2)
print("PASS: PostToolUse hook registered")
PYEOF
Tell the user:
Hooks take effect on the next Claude Code session.
To test them, you'll need to restart:
1. Exit this session (Ctrl+C or /exit)
2. Relaunch: claude .
3. Re-run this module: /learn-10-hooks
On re-entry, the preflight will show the hooks are configured.
Then try:
- Ask Claude to "run git push --force" — should be blocked
- Ask Claude to edit any file — the audit log should capture it
After restart, verify hooks are active:
# Check hooks are registered
python3 -c "
import json
d = json.load(open('.claude/settings.local.json'))
hooks = d.get('hooks', {})
pre = len(hooks.get('PreToolUse', []))
post = len(hooks.get('PostToolUse', []))
print(f'PreToolUse hooks: {pre}')
print(f'PostToolUse hooks: {post}')
" 2>/dev/null
# Check if audit log exists (will exist after an edit)
if [ -f ".claude/logs/edit-audit.log" ]; then
echo "EXISTS: Audit log has $(wc -l < .claude/logs/edit-audit.log | tr -d ' ') entries"
else
echo "INFO: Audit log will be created after the first file edit"
fi
Run all checks as PASS/FAIL:
PASS=0
TOTAL=4
# 1. Hook scripts exist
[ -x ".claude/hooks/guard-git.sh" ] && { echo "PASS: Guard hook script exists and is executable"; PASS=$((PASS+1)); } || echo "FAIL: Guard hook script missing"
# 2. Logging hook exists
[ -x ".claude/hooks/log-edits.sh" ] && { echo "PASS: Logging hook script exists and is executable"; PASS=$((PASS+1)); } || echo "FAIL: Logging hook script missing"
# 3. PreToolUse hook registered
python3 -c "
import json
d = json.load(open('.claude/settings.local.json'))
hooks = d.get('hooks', {}).get('PreToolUse', [])
assert any('guard-git' in h.get('command','') for h in hooks)
print('PASS: PreToolUse hook registered')
" 2>/dev/null && PASS=$((PASS+1)) || echo "FAIL: PreToolUse hook not registered"
# 4. PostToolUse hook registered
python3 -c "
import json
d = json.load(open('.claude/settings.local.json'))
hooks = d.get('hooks', {}).get('PostToolUse', [])
assert any('log-edits' in h.get('command','') for h in hooks)
print('PASS: PostToolUse hook registered')
" 2>/dev/null && PASS=$((PASS+1)) || echo "FAIL: PostToolUse hook not registered"
echo ""
echo "$PASS/$TOTAL checks passed."
If all pass, print:
All checks passed. Hooks are configured and ready.
If any fail, tell the user which step to re-run.
Create a new hook that does one of the following:
Option A: A PreToolUse hook that warns (but doesn't block)
when a Bash command would modify files outside the current
project directory.
Option B: A PostToolUse hook that automatically runs
"git diff --stat" after every file edit.
Tell me:
1. Which option you chose
2. Show me your hook script
3. Show me the settings.json entry you added
Review the user's hook script and settings entry. Check:
Write the completion marker:
date -u +%Y-%m-%dT%H:%M:%SZ > ~/.claude/courseware-progress/10.done
If successful, print:
Module 10 complete.
You can now create Claude Code hooks for guardrails and automation.
Key concepts:
- PreToolUse: runs before, exit 2 = block, exit 0 = allow
- PostToolUse: runs after, receives tool result
- Hooks receive JSON on stdin with tool_name and tool_input
- Matcher uses regex to select which tools trigger the hook
- Settings scope: global (~/.claude/), project (.claude/), local (.local)
Common hook patterns:
- Block dangerous commands (force push, rm -rf /)
- Audit trail of all file modifications
- Auto-lint after code changes
- Notify on long-running operations
- Enforce naming conventions
Next module: /learn-11-building-mcp-servers
Questions or feedback? https://github.com/rhpds/claude-code-courseware/issues