Hooks are scripts that execute automatically on Claude Code events. They enable automation, block dangerous operations, and enrich context.
| Hook | Event | Purpose | Platform |
|---|---|---|---|
| dangerous-actions-blocker.sh | PreToolUse | Block dangerous commands/edits | Bash |
| security-check.sh | PreToolUse | Block secrets in commands | Bash |
| prompt-injection-detector.sh | PreToolUse | Detect injection attempts (+ANSI, null bytes, nested cmd) | Bash |
| unicode-injection-scanner.sh | PreToolUse | Detect zero-width, RTL override, ANSI escape, null bytes | Bash |
| repo-integrity-scanner.sh | PreToolUse | Scan README/package.json for hidden injection | Bash |
| mcp-config-integrity.sh | SessionStart | Verify MCP config hash (CVE-2025-54135/54136) | Bash |
| claudemd-scanner.sh | SessionStart | Detect CLAUDE.md injection attacks | Bash |
| output-secrets-scanner.sh | PostToolUse | Detect secrets + env leakage in tool outputs | Bash |
| auto-format.sh | PostToolUse | Auto-format after edits | Bash |
| learning-capture.sh | Stop | Prompt for daily learning capture | Bash |
| notification.sh | Notification | Contextual macOS sound alerts | Bash (macOS) |
| security-check.ps1 | PreToolUse | Block secrets in commands | PowerShell |
| auto-format.ps1 | PostToolUse | Auto-format after edits | PowerShell |
| Event | When | Typical Use Cases |
|---|---|---|
PreToolUse |
Before a tool executes | Validation, blocking dangerous operations |
PostToolUse |
After a tool executes | Formatting, logging, cleanup |
UserPromptSubmit |
When user sends a message | Context enrichment, preprocessing |
Notification |
When Claude sends a notification | Sound alerts, external notifications |
SessionStart |
At session start | Initialization, environment setup |
SessionEnd |
At session end | Cleanup, session summary |
Stop |
User interrupts operation | State saving, graceful shutdown |
Advanced protection patterns inspired by production LLM systems.
Event: PreToolUse
Detects and blocks prompt injection attempts before they reach Claude:
Detected Patterns:
- Role override: "ignore previous instructions", "you are now", "pretend to be"
- Jailbreak attempts: "DAN mode", "developer mode", "no restrictions"
- Delimiter injection:
</system>,[INST],<<SYS>> - Authority impersonation: "anthropic employee", "authorized to bypass"
- Base64-encoded payloads (decoded and scanned)
- Context manipulation: false claims about previous messages
Configuration:
{
"hooks": {
"PreToolUse": [{
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/prompt-injection-detector.sh",
"timeout": 5000
}]
}]
}
}Event: PostToolUse
Heuristic validation of Claude's outputs (no LLM call, pure bash):
Validation Checks:
- Placeholder paths:
/path/to/,/your/project/ - Placeholder content:
TODO:,your-api-key,example.com - Potential secrets in output (regex patterns)
- Uncertainty indicators (multiple "I'm not sure", "probably")
- Incomplete implementations:
NotImplementedError,throw new Error - Unverified reference claims
Behavior: Warns via systemMessage, does not block. For deeper validation, use the output-evaluator agent.
Event: PostToolUse
Logs all Claude operations to JSONL files for monitoring and cost tracking:
Log Location: ~/.claude/logs/activity-YYYY-MM-DD.jsonl
Logged Data:
- Timestamp, session ID, tool name
- File paths and commands (truncated)
- Project name
- Token estimates (input/output)
Analysis: Use session-stats.sh script to analyze logs.
Environment Variables:
| Variable | Default | Description |
|---|---|---|
CLAUDE_LOG_DIR |
~/.claude/logs |
Log directory |
CLAUDE_LOG_TOKENS |
true |
Enable token estimation |
CLAUDE_SESSION_ID |
auto | Custom session ID |
See Observability Guide for full documentation.
Type: Git pre-commit hook (not Claude hook)
LLM-as-a-Judge evaluation before every commit. Opt-in only due to API costs.
Installation:
cp pre-commit-evaluator.sh .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
export CLAUDE_PRECOMMIT_EVAL=1 # Enable evaluationCost: ~$0.01-0.05 per commit (Haiku model)
Bypass: git commit --no-verify or CLAUDE_SKIP_EVAL=1 git commit
Event: PreToolUse (Bash, Edit, Write)
Comprehensive protection against dangerous operations:
Bash - Blocked Commands:
- System destruction:
rm -rf /,rm -rf ~,sudo rm - Disk operations:
dd if=,mkfs,> /dev/sda - Fork bombs:
:(){:|:&};: - Database drops:
DROP DATABASE,DROP TABLE - Force pushes:
git push --force main/master - Package publishing:
npm publish,pnpm publish - Secret patterns:
password=,api_key=,token=
Edit/Write - Protected Files:
- Environment:
.env,.env.local,.env.production - Credentials:
credentials.json,serviceAccountKey.json - SSH keys:
id_rsa,id_ed25519,id_ecdsa - Config:
.npmrc,.pypirc,secrets.yml
Edit/Write - Allowed Paths:
$CLAUDE_PROJECT_DIR(current project)~/.claude/(Claude config)/tmp/(temporary files)- Additional paths via
$ALLOWED_PATHSenvironment variable
Exit Codes:
exit 0 # Allow operation
exit 2 # Block (stderr message shown to Claude)Configuration:
# Add custom allowed paths (colon-separated)
export ALLOWED_PATHS="/custom/path:/another/path"Event: PreToolUse (Bash)
Focused on detecting secrets in commands:
- Password patterns
- API keys (common formats like
sk-xxx,pk-xxx) - AWS credentials
- Private keys
- Hardcoded tokens
Event: SessionStart
Scans CLAUDE.md files at session start for potential prompt injection attacks:
Detected Patterns:
- "ignore previous instructions" variants
- Shell injection:
curl | bash,wget | sh,eval( - Base64 encoded content (potential obfuscation)
- Hidden instructions in HTML comments
- Suspicious long lines (>500 chars)
- Non-ASCII characters near sensitive keywords (homoglyph attacks)
Files Scanned:
CLAUDE.md(project root).claude/CLAUDE.md(local override)- Any
.mdfiles in.claude/directory
Why This Matters: When you clone an unfamiliar repository, a malicious CLAUDE.md could inject instructions that compromise your system. This hook warns you before Claude processes potentially dangerous instructions.
Configuration:
{
"hooks": {
"SessionStart": [{
"hooks": [{
"type": "command",
"command": ".claude/hooks/claudemd-scanner.sh",
"timeout": 5000
}]
}]
}
}Event: PostToolUse
Complements security-check.sh by scanning tool outputs (not inputs) for leaked secrets.
Detected Patterns:
- API Keys: OpenAI, Anthropic, AWS, GCP, Azure, Stripe, Twilio, SendGrid
- Tokens: GitHub, GitLab, NPM, PyPI, JWT
- Private Keys: RSA, EC, DSA, OpenSSH, PGP
- Database URLs with embedded passwords
- Generic
api_key=,secret=,password=patterns
Why This Matters: Claude might read a .env file and include credentials in its response or a commit. This hook catches secrets before they leak.
Configuration:
{
"hooks": {
"PostToolUse": [{
"hooks": [{
"type": "command",
"command": ".claude/hooks/output-secrets-scanner.sh",
"timeout": 5000
}]
}]
}
}Event: PostToolUse (Edit, Write)
Automatically format files after editing:
| Extension | Formatter |
|---|---|
.ts, .tsx, .js, .jsx |
Prettier |
.json, .css, .scss, .md |
Prettier |
.prisma |
prisma format |
.py |
Black / autopep8 |
.go |
go fmt |
Silent Operation: No output, failures ignored to avoid blocking Claude.
Requirements: Install formatters in your project:
# Node.js projects
npm install -D prettier
# Python projects
pip install black
# Go projects (built-in)
go fmtEvent: Notification (macOS only)
Contextual sound alerts based on notification content:
| Context | Sound | Triggered By |
|---|---|---|
| Success | Hero.aiff | "completed", "done", "success" |
| Error | Basso.aiff | "error", "failed", "problem" |
| Waiting | Submarine.aiff | "waiting", "permission", "input" |
| Warning | Sosumi.aiff | "warning", "attention", "alert" |
| Default | Ping.aiff | Other notifications |
Features:
- Non-blocking (plays in background)
- Native macOS notifications
- Automatic context detection via keywords
- Multi-language support (English/French)
Requirements: macOS with afplay and osascript (built-in)
Hooks are configured in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/dangerous-actions-blocker.sh",
"timeout": 5000
}]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-format.sh",
"timeout": 10000
}]
}
],
"Notification": [
{
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notification.sh",
"timeout": 5000
}]
}
]
}
}Matcher Patterns:
".*"- Match all tools"Bash"- Match only Bash tool"Edit|Write"- Match Edit OR Write tools"Bash|Edit|Write"- Match multiple tools
#!/bin/bash
# Hook: [EventType] - Description
# Exit 0 = success/allow, Exit 2 = block (PreToolUse only)
set -e
# Read JSON from stdin
INPUT=$(cat)
# Extract data
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // empty')
# Your logic here...
# Return optional JSON
cat << EOF
{
"systemMessage": "Message displayed to Claude",
"hookSpecificOutput": {
"additionalContext": "Extra context added"
}
}
EOF
exit 0Available in hook scripts:
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR |
Current project path |
CLAUDE_FILE_PATHS |
Files passed with -f flag |
CLAUDE_TOOL_INPUT |
Tool input as JSON |
HOME |
User home directory |
- Short Timeout: Max 5-10s to avoid blocking Claude
- Fail Gracefully: Use
|| truefor non-critical operations - Minimal Logging: Avoid stdout except structured JSON
- Require jq: Parse JSON with
jqfor reliability - Test Thoroughly: Test with various inputs before deploying
- Document Behavior: Clear comments on what hook does
- Handle Errors: Proper error messages for debugging
Create git-context.sh (UserPromptSubmit event):
#!/bin/bash
# Hook: UserPromptSubmit - Add Git context to prompts
set -e
INPUT=$(cat)
# Get Git information
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
LAST_COMMIT=$(git log -1 --oneline 2>/dev/null || echo "none")
UNCOMMITTED=$(git status --short 2>/dev/null | wc -l | tr -d ' ')
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
# Return enriched context
cat << EOF
{
"systemMessage": "[Git] Branch: $BRANCH | Last: $LAST_COMMIT | Uncommitted: $UNCOMMITTED files | Staged: $STAGED files"
}
EOF
exit 0Register in settings:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/git-context.sh",
"timeout": 3000
}]
}
]
}
}- Create hooks directory:
mkdir -p .claude/hooks- Copy hook from examples:
cp /path/to/examples/hooks/bash/dangerous-actions-blocker.sh .claude/hooks/
chmod +x .claude/hooks/*.sh-
Configure in
.claude/settings.json(see Configuration section above) -
Commit to repository:
git add .claude/hooks/ .claude/settings.json
git commit -m "Add Claude Code hooks"- Create global hooks directory:
mkdir -p ~/.claude/hooks- Copy hook:
cp /path/to/examples/hooks/bash/notification.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.sh- Configure in
~/.claude/settings.json
Priority: Project hooks override global hooks.
- Use
.shextension - Requires
chmod +xfor execution - Path separator:
/ - Home directory:
~or$HOME
- Use
.ps1extension - May require execution policy:
Set-ExecutionPolicy RemoteSigned - Path separator:
\ - Home directory:
$env:USERPROFILE
Cause: Not registered in settings.json or wrong path
Fix: Verify configuration and use absolute paths or $CLAUDE_PROJECT_DIR
Cause: Hook not executable
Fix:
chmod +x .claude/hooks/*.shCause: Exit code 2 without conditions
Fix: Check logic, ensure exit 0 is default case
Cause: Hook takes too long (>timeout value)
Fix: Optimize hook performance or increase timeout in settings
Cause: jq not installed
Fix: Install jq:
# macOS
brew install jq
# Ubuntu/Debian
sudo apt-get install jq
# Windows
choco install jqLog all Claude operations to JSONL file:
#!/bin/bash
# PostToolUse - Log all activities
set -e
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
LOG_FILE="$HOME/.claude/logs/activity-$(date +%Y-%m-%d).jsonl"
mkdir -p "$(dirname "$LOG_FILE")"
# Create log entry
cat << EOF >> "$LOG_FILE"
{"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","tool":"$TOOL","session":"$CLAUDE_SESSION_ID"}
EOF
exit 0Alert when migrations are created:
#!/bin/bash
# PostToolUse - Detect database migrations
set -e
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
if [[ "$TOOL" == "Write" ]]; then
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
if [[ "$FILE" == *"/migrations/"* ]]; then
cat << EOF
{
"systemMessage": "⚠️ Database migration created: $FILE\n\nReminder:\n1. Review migration carefully\n2. Test on dev database first\n3. Run 'prisma migrate deploy' after merge"
}
EOF
fi
fi
exit 0- Never Store Secrets in Hooks: Use environment variables
- Validate Input: Always sanitize data from stdin
- Limit Hook Scope: Use specific matchers, not
".*" - Review Blocked Operations: Log when hooks block actions
- Test in Isolation: Test hooks outside Claude first
- Version Control: Commit hooks to repository for team sharing
See the main guide for detailed explanations and advanced patterns.