hooks

Installation
SKILL.md

Hooks

Hooks are Claude Code's automation surface — deterministic bash (or any shell) scripts that fire on lifecycle events. Each hook receives JSON on stdin and returns JSON on stdout: {"decision": "approve"|"block", "reason"?: "..."}.

Events (matcher + input + output)

Event Fires Input Output shape
PreToolUse Before any tool {tool_name, tool_input} {decision, reason?} — block prevents the tool call
PostToolUse After tool succeeds {tool_name, tool_input, tool_output} {decision, reason?}
PostToolUseFailure After tool fails {tool_name, tool_input, error} {decision} — almost always approve
Notification Claude needs input {message} {decision}
Stop Agent finishes turn {stop_reason} {decision}
UserPromptSubmit Prompt submitted {prompt} {decision} + optional modified prompt
SessionStart New session {} {decision} — usually for context injection

Installing a hook pack

Use MCP cc_kb_hook_recipe(name) to fetch a security-hardened pack. Each pack returns:

  • script — the bash script content
  • script_path — target path (.claude/hooks/{name}.sh)
  • settings_snippet — the JSON to merge into .claude/settings.json
  • verify — one-line manual test

Available pack names (fetch a shortlist first via cc_docs_hook_pack_recommend(signals)):

  • protect-sensitive-files — block writes to .env/credentials (always recommended)
  • auto-format-after-edit — prettier/black/rustfmt on Write|Edit
  • stop-until-tests-pass — block Stop if tests fail
  • post-compact-context-restoration — re-load memory rules after /compact
  • direnv-reload-on-cwd-change — reload .envrc on cd
  • task-created-governance — log new tasks to memory
  • task-completed-quality-gate — enforce lint+test before task completion
  • teammate-idle-enforcement — prevent idle teammate processes

Authoring a new hook

#!/bin/bash
set -euo pipefail
INPUT=$(head -c 65536)

# Validate JSON input
if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then
  echo '{"decision":"approve"}'; exit 0
fi

# Extract fields you need
FILE=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')

# Validate path (prevent injection + traversal)
[ -z "$FILE" ] && { echo '{"decision":"approve"}'; exit 0; }
REAL=$(realpath "$FILE" 2>/dev/null) || { echo '{"decision":"approve"}'; exit 0; }
WD=$(realpath "$PWD")
[[ "$REAL" != "$WD"/* ]] && { echo '{"decision":"approve"}'; exit 0; }
BN=$(basename "$REAL")
[[ "$BN" == -* ]] && { echo '{"decision":"approve"}'; exit 0; }

# Do the work...

# Always emit valid JSON
echo '{"decision":"approve"}'

Safety rules:

  1. Always cap input with head -c.
  2. Always validate JSON before parsing.
  3. Always realpath + PWD-prefix check before touching a file.
  4. Reject filenames starting with - (flag injection).
  5. Never eval or unquoted-interpolate user content.
  6. Default to {"decision":"approve"} on any error — never block on bugs.
  7. For concurrent writes to shared files (e.g. a log), use flock.

Registering in settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "bash .claude/hooks/protect-sensitive-files.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "bash .claude/hooks/auto-format-after-edit.sh" }]
      }
    ]
  }
}

Matchers are regex. Write|Edit fires for both tools. * fires for all tools.

Debugging

  1. Hook not firing: check .claude/settings.json has the event + matcher. Matcher regex is case-sensitive.
  2. Hook blocks unexpectedly: check the hook's stderr output — Claude Code prints it.
  3. Hook slow: profile with time bash .claude/hooks/.... Target <100ms for PreToolUse, <500ms for PostToolUse.
  4. JSON parse error: run bash .claude/hooks/x.sh < fixture.json locally.

MCP delegation

Need Tool
Fetch a specific hook pack cc_kb_hook_recipe(name)
Recommend packs from signals cc_docs_hook_pack_recommend({has_formatter, has_tests, has_secrets, has_git, ...})

Anti-patterns

  • Hooks that call LLMs → slow and non-deterministic. Hooks should be fast local computation.
  • Hooks that rm -rf or delete files → use Bash tool with user confirmation, not a hook.
  • Hooks that swallow errors without emitting approve → hangs the session.
  • Multiple hooks on same event that fight each other → run them sequentially in one script or use distinct matchers.

Reference

Related skills
Installs
6
GitHub Stars
11
First Seen
Apr 19, 2026