claude-code-hooks

Claude Code Hooks — Meta Reference

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "claude-code-hooks" with this command: npx skills add vasilyu1983/ai-agents-public/vasilyu1983-ai-agents-public-claude-code-hooks

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

SessionStart

Session begins/resumes Initialize environment

UserPromptSubmit

User submits prompt Preprocess/validate input

PreToolUse

Before tool execution Validate, block dangerous commands

PermissionRequest

Permission dialog shown Auto-allow/deny permissions

PostToolUse

After tool succeeds Format, audit, notify

PostToolUseFailure

After tool fails Capture failures, add guidance

SubagentStart

Subagent spawns Inspect subagent metadata

Stop

When Claude finishes Run tests, summarize

SubagentStop

Subagent finishes Verify subagent completion

Notification

On notifications Alert integrations

PreCompact

Before context compaction Preserve critical context

Setup

--init /--maintenance

Initialize repo/env

SessionEnd

Session ends Cleanup, save state

Hook Structure

.claude/hooks/ ├── pre-tool-validate.sh ├── post-tool-format.sh ├── post-tool-audit.sh ├── stop-run-tests.sh └── session-start-init.sh

Configuration

settings.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)

{ "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "ls -la" } }

Environment Variables (shell)

Variable Description

CLAUDE_PROJECT_DIR

Absolute project root where Claude Code started

CLAUDE_PLUGIN_ROOT

Plugin root (plugin hooks only)

CLAUDE_CODE_REMOTE

"true" in remote/web environments; empty/local otherwise

CLAUDE_ENV_FILE

File path to persist export ... lines (available in SessionStart; check docs for Setup support)

Exit Codes

Code Meaning Notes

0

Success JSON written to stdout is parsed for structured control

2

Blocking error stderr becomes the message; JSON in stdout is ignored

Other Non-blocking error Execution continues; stderr is visible in verbose mode

Stdout injection note: for UserPromptSubmit , SessionStart , and Setup , non-JSON stdout (exit 0) is injected into Claude’s context; most other events show stdout only in verbose mode.

Decision Control + Input Modification (v2.0.10+)

PreToolUse hooks can allow/deny/ask and optionally modify the tool input via updatedInput .

Hook Output Schema

{ "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 decision /reason fields are deprecated; prefer the hookSpecificOutput.* fields.

Example: Redirect Sensitive File Edits

#!/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

#!/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 (type: "prompt" ) instead of bash scripts. They are most useful for Stop and SubagentStop decisions.

Configuration

{ "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:

{ "Stop": [ { "hooks": [ { "type": "command", "command": ".claude/hooks/quick-check.sh" }, { "type": "prompt", "prompt": "Verify code quality meets standards" } ] } ] }

Hook Templates

Pre-Tool Validation

#!/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 0

Post-Tool Formatting

#!/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 0

Post-Tool Security Audit

#!/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 0

Stop Hook (Run Tests)

#!/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 0

Session Start

#!/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 0

Matchers

Matchers filter which tool triggers the hook:

  • Exact match: Write matches only the Write tool

  • Regex: Edit|Write or Notebook.*

  • Match all: * (also works with "" or omitted matcher)

Security Best Practices

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 deploying

Hook Composition

Multiple Hooks on Same Event

{ "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

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

  • ../claude-code-commands/SKILL.md — Command creation

  • ../claude-code-agents/SKILL.md — Agent creation

  • ../ops-devops-platform/SKILL.md — CI/CD integration

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

software-clean-code-standard

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

docs-codebase

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

software-code-review

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ops-devops-platform

No summary provided by upstream source.

Repository SourceNeeds Review