agents-hooks
Original:🇺🇸 English
Translated
Create event-driven hooks for AI coding agent automation (Claude Code, Codex CLI). Configure hook events in settings or frontmatter, parse stdin JSON inputs, return decision-control JSON, and implement secure hook scripts.
2installs
Added on
NPX Install
npx skill4agent add vasilyu1983/ai-agents-public agents-hooksTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Claude Code Hooks — Meta Reference
This skill provides the definitive reference for creating Claude Code hooks. Use this when building automation that triggers on Claude Code events.
When to Use This Skill
- Building event-driven automation for Claude Code
- Creating PreToolUse guards to block dangerous commands
- Implementing PostToolUse formatters, linters, or auditors
- Adding Stop hooks for testing or notifications
- Setting up SessionStart/SessionEnd for environment management
- Integrating Claude Code with CI/CD pipelines (headless mode)
Quick Reference
| Event | Trigger | Use Case |
|---|---|---|
| Session begins/resumes | Initialize environment |
| User submits prompt | Preprocess/validate input |
| Before tool execution | Validate, block dangerous commands |
| Permission dialog shown | Auto-allow/deny permissions |
| After tool succeeds | Format, audit, notify |
| After tool fails | Capture failures, add guidance |
| Subagent spawns | Inspect subagent metadata |
| When Claude finishes | Run tests, summarize |
| Subagent finishes | Verify subagent completion |
| On notifications | Alert integrations |
| Before context compaction | Preserve critical context |
| | Initialize repo/env |
| Session ends | Cleanup, save state |
Hook Structure
text
.claude/hooks/
├── pre-tool-validate.sh
├── post-tool-format.sh
├── post-tool-audit.sh
├── stop-run-tests.sh
└── session-start-init.shConfiguration
settings.json
json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/post-tool-format.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-tool-validate.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-run-tests.sh"
}
]
}
]
}
}Execution Model (Jan 2026)
- Hooks receive a JSON payload via stdin (treat it as untrusted input) and run with your user permissions (outside the Bash tool sandbox).
- Default timeout is 60s per hook command; all matching hooks run in parallel; identical commands are deduplicated.
Hook Input (stdin)
json
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "ls -la"
}
}Environment Variables (shell)
| Variable | Description |
|---|---|
| Absolute project root where Claude Code started |
| Plugin root (plugin hooks only) |
| |
| File path to persist |
Exit Codes
| Code | Meaning | Notes |
|---|---|---|
| Success | JSON written to stdout is parsed for structured control |
| Blocking error | |
| Other | Non-blocking error | Execution continues; |
Stdout injection note: for , , and , non-JSON stdout (exit 0) is injected into Claude’s context; most other events show stdout only in verbose mode.
UserPromptSubmitSessionStartSetupDecision Control + Input Modification (v2.0.10+)
PreToolUse hooks can allow/deny/ask and optionally modify the tool input via .
updatedInputHook Output Schema
json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Reason shown to user (and to Claude on deny)",
"updatedInput": { "command": "echo 'modified'" },
"additionalContext": "Extra context added before tool runs"
}
}Note: older / fields are deprecated; prefer the fields.
decisionreasonhookSpecificOutput.*Example: Redirect Sensitive File Edits
bash
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
# Redirect package-lock.json edits to /dev/null
if [[ "$FILE_PATH" == *"package-lock.json" ]]; then
UPDATED_INPUT="$(echo "$INPUT" | jq -c '.tool_input | .file_path = "/dev/null"')"
jq -cn --argjson updatedInput "$UPDATED_INPUT" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
permissionDecisionReason: "Redirected write to /dev/null",
updatedInput: $updatedInput
}
}'
exit 0
fi
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'Example: Strip Sensitive Files from Git Add
bash
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name')"
CMD="$(echo "$INPUT" | jq -r '.tool_input.command // empty')"
if [[ "$TOOL_NAME" == "Bash" && "$CMD" =~ ^git[[:space:]]+add ]]; then
# Remove .env files from staging
SAFE_CMD="$(echo "$CMD" | sed 's/\.env[^ ]*//g')"
if [[ "$SAFE_CMD" != "$CMD" ]]; then
echo '{}' | jq -cn --arg cmd "$SAFE_CMD" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow",
permissionDecisionReason: "Removed .env from git add",
updatedInput: { command: $cmd }
}
}'
exit 0
fi
fi
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}}'Prompt-Based Hooks
For complex decisions, use LLM-evaluated hooks () instead of bash scripts. They are most useful for and decisions.
type: "prompt"StopSubagentStopConfiguration
json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Evaluate whether Claude should stop. Context JSON: $ARGUMENTS. Return {\"ok\": true} if all tasks are complete, otherwise {\"ok\": false, \"reason\": \"what remains\"}.",
"timeout": 30
}
]
}
]
}
}Response Schema
- Allow:
{"ok": true} - Block:
{"ok": false, "reason": "Explanation shown to Claude"}
Combining Command and Prompt Hooks
Use command hooks for fast, deterministic checks. Use prompt hooks for nuanced decisions:
json
{
"Stop": [
{
"hooks": [
{ "type": "command", "command": ".claude/hooks/quick-check.sh" },
{ "type": "prompt", "prompt": "Verify code quality meets standards" }
]
}
]
}Permission Prompt Fatigue Reduction Pattern
Use this when frequent approval dialogs slow down repeated safe workflows.
Strategy
- Identify repetitive, low-risk command prefixes (for example: test runners, read-only diagnostics).
- Approve narrow prefixes instead of full commands.
- Keep destructive or broad shells (,
rm, generic interpreters with arbitrary input) out of auto-approval rules.git reset --hard - Re-check approved prefixes periodically; remove stale ones.
Practical Guardrails
- Allow only task-scoped prefixes (example: ), not unrestricted executors.
npm run test:e2e - Keep separate policy for write-outside-workspace actions.
- Pair allow-rules with deny-rules for dangerous patterns.
Outcome
This reduces repeated permission interruptions while preserving high-safety boundaries.
Hook Templates
Pre-Tool Validation
bash
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name')"
CMD="$(echo "$INPUT" | jq -r '.tool_input.command // empty')"
if [[ "$TOOL_NAME" == "Bash" ]]; then
# Block rm -rf /
if echo "$CMD" | grep -qE 'rm\s+-rf\s+/'; then
echo '{}' | jq -cn '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Dangerous rm command detected"
}
}'
exit 0
fi
# Block force push to main
if echo "$CMD" | grep -qE 'git\s+push.*--force.*(main|master)'; then
echo '{}' | jq -cn '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Force push to main/master not allowed"
}
}'
exit 0
fi
# Soft-warning: possible credential exposure
if echo "$CMD" | grep -qE '(password|secret|api_key)\s*='; then
echo '{}' | jq -cn '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: "Possible credential exposure in command",
additionalContext: "Command may include a secret. Confirm intent and avoid committing secrets."
}
}'
exit 0
fi
fi
exit 0Post-Tool Formatting
bash
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name')"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
if [[ "$TOOL_NAME" =~ ^(Edit|Write)$ && -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
case "$FILE_PATH" in
*.js|*.ts|*.jsx|*.tsx|*.json|*.md)
npx prettier --write "$FILE_PATH" 2>/dev/null || true
;;
*.py)
ruff format "$FILE_PATH" 2>/dev/null || true
;;
*.go)
gofmt -w "$FILE_PATH" 2>/dev/null || true
;;
*.rs)
rustfmt "$FILE_PATH" 2>/dev/null || true
;;
esac
fi
exit 0Post-Tool Security Audit
bash
#!/bin/bash
set -euo pipefail
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name')"
FILE_PATH="$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')"
if [[ "$TOOL_NAME" =~ ^(Edit|Write)$ && -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
# Check for hardcoded secrets
if grep -qE '(password|secret|api_key|token)\s*[:=]\s*["\x27][^"\x27]+["\x27]' "$FILE_PATH"; then
echo "WARNING: Possible hardcoded secret in $FILE_PATH" >&2
fi
# Check for console.log in production code
if [[ "$FILE_PATH" =~ \.(ts|js|tsx|jsx)$ ]] && grep -q 'console.log' "$FILE_PATH"; then
echo "NOTE: console.log found in $FILE_PATH" >&2
fi
fi
exit 0Stop Hook (Run Tests)
bash
#!/bin/bash
set -euo pipefail
# Run tests after Claude finishes
cd "$CLAUDE_PROJECT_DIR"
# Detect test framework
if [[ -f "package.json" ]]; then
if grep -q '"vitest"' package.json; then
npm run test 2>&1 | head -50
elif grep -q '"jest"' package.json; then
npm test 2>&1 | head -50
fi
elif [[ -f "pytest.ini" ]] || [[ -f "pyproject.toml" ]]; then
pytest --tb=short 2>&1 | head -50
fi
exit 0Session Start
bash
#!/bin/bash
set -euo pipefail
cd "$CLAUDE_PROJECT_DIR"
# Check git status
echo "=== Git Status ==="
git status --short
# Check for uncommitted changes
if ! git diff --quiet; then
echo "WARNING: Uncommitted changes detected"
fi
# Verify dependencies
if [[ -f "package.json" ]]; then
if [[ ! -d "node_modules" ]]; then
echo "NOTE: node_modules missing, run npm install"
fi
fi
exit 0Matchers
Matchers filter which tool triggers the hook:
- Exact match: matches only the Write tool
Write - Regex: or
Edit|WriteNotebook.* - Match all: (also works with
*or omitted matcher)""
Security Best Practices
text
HOOK SECURITY CHECKLIST
[ ] Validate all inputs with regex
[ ] Quote all variables: "$VAR" not $VAR
[ ] Use absolute paths
[ ] No eval with untrusted input
[ ] Set -euo pipefail at top
[ ] Keep hooks fast (<1 second)
[ ] Log actions for audit
[ ] Test manually before deployingHook Composition
Multiple Hooks on Same Event
json
{
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": ".claude/hooks/format.sh" },
{ "type": "command", "command": ".claude/hooks/audit.sh" },
{ "type": "command", "command": ".claude/hooks/notify.sh" }
]
}
]
}All matching hooks run in parallel. If you need strict ordering (format → lint → test), make one wrapper script that runs them sequentially.
Debugging Hooks
bash
# Test a PostToolUse hook manually (stdin JSON)
export CLAUDE_PROJECT_DIR="$(pwd)"
echo '{"hook_event_name":"PostToolUse","tool_name":"Edit","tool_input":{"file_path":"'"$(pwd)"'/src/app.ts"}}' \
| bash .claude/hooks/post-tool-format.sh
# Check exit code
echo $?Navigation
Resources
- references/hook-patterns.md — Common patterns
- references/hook-security.md — Security guide
- data/sources.json — Documentation links
Related Skills
- ../agents-subagents/SKILL.md — Agent creation
- ../agents-skills/SKILL.md — Skill creation
- ../ops-devops-platform/SKILL.md — CI/CD integration