These pages document master, which is unreleased and in development. The Quick Start installs the latest stable release; anything newer than that tag is marked in the text.
Hooks Guide
What Ships With This Configuration
Section titled “What Ships With This Configuration”Six hook scripts that enforce rules deterministically instead of relying on the LLM to remember them.
| Hook | Event | What It Does |
|---|---|---|
verify-on-stop.sh |
Stop | Runs the project test suite when Claude finishes. If tests fail, feeds errors back so Claude continues fixing. |
verify-after-edit.sh |
PostToolUse (Edit|Write) | Runs the project type checker after source file edits. Feeds type errors back immediately. |
auto-format.sh |
PostToolUse (Edit|Write) | Runs the project formatter (Prettier, Black, gofmt, rustfmt) after source edits. Fire-and-forget. |
protect-files.sh |
PreToolUse (Edit|Write) | Blocks edits to .env, *.lock, .git/, credentials. Returns denial reason to Claude. |
reinject-context.sh |
SessionStart | Re-injects project context (git status, Ralph Loop PRD, pending work) on session start/compaction. |
notify.sh |
Notification | Sends desktop notifications (macOS/Linux) when Claude needs input. |
All hooks auto-detect your project’s stack — no configuration per language needed.
Installation
Section titled “Installation”All three hooks are installed automatically by claude-setup.sh:
./claude-setup.shThis installs:
- Hook scripts to
~/.claude/hooks/ - Hook wiring to
~/.claude/settings.json
The hooks are globally active in all Claude Code sessions immediately after setup. No per-project configuration needed.
Source scripts are also available in .claude/hooks/ within this repository for reference.
How Hooks Work
Section titled “How Hooks Work”Hooks are shell scripts that Claude Code runs at specific lifecycle events.
Input: JSON on stdin containing session info and event-specific fields.
Exit codes:
0— success; Claude proceeds normally2— block/feedback; effect depends on event type:- Stop hooks: Claude continues working (doesn’t stop)
- PostToolUse hooks: stderr content is fed back to Claude as context
- Any other code — hook failure (logged, Claude proceeds)
Output:
- stdout — data passed back to Claude Code (JSON or plain text)
- stderr — error messages fed to Claude (on exit code 2) or logged
Verifying the Hooks Are Active
Section titled “Verifying the Hooks Are Active”After running claude-setup.sh, confirm the hooks are installed:
ls -la ~/.claude/hooks/cat ~/.claude/settings.jsonYou should see all three scripts in ~/.claude/hooks/ and the hooks wiring in ~/.claude/settings.json.
Disabling Individual Hooks
Section titled “Disabling Individual Hooks”Remove the hook entry from .claude/settings.json (project) or ~/.claude/settings.json (global).
The hook scripts can remain on disk — they only run when wired in settings.json. To temporarily disable without editing JSON, rename the script:
mv .claude/hooks/verify-on-stop.sh .claude/hooks/verify-on-stop.sh.disabledTo disable all hooks at once, add to settings.json:
{ "disableAllHooks": true}Customizing
Section titled “Customizing”Timeouts
Section titled “Timeouts”Each hook has a timeout field in settings.json (milliseconds):
| Hook | Default | When to increase |
|---|---|---|
verify-on-stop.sh |
180000 (3 min) | Slow test suites |
verify-after-edit.sh |
30000 (30 sec) | Large TypeScript projects |
auto-format.sh |
15000 (15 sec) | Large files or slow formatters |
protect-files.sh |
5000 (5 sec) | Rarely needed |
reinject-context.sh |
10000 (10 sec) | Large repos with many context sources |
notify.sh |
10000 (10 sec) | Rarely needed |
Blocking vs report-only mode
Section titled “Blocking vs report-only mode”The two verification hooks have different default behaviors:
HOOK_EDIT_BLOCK=true(default) — edit-time verification blocks on errors. Claude sees type/lint errors after each edit and automatically tries to fix them. This is targeted and useful.HOOK_STOP_BLOCK=false(default) — stop-time verification is report-only. Claude reports test results but doesn’t enter a fix loop. This prevents test-fix loops in projects with pre-existing failures.
To enable blocking on stop (for projects with a clean test suite):
{ "env": { "HOOK_STOP_BLOCK": "true" }}Or override per-session via shell before launching Claude:
export HOOK_STOP_BLOCK=trueclaudeTo disable edit-time blocking (report-only for all hooks):
{ "env": { "HOOK_EDIT_BLOCK": "false", "HOOK_STOP_BLOCK": "false" }}Internal test timeout
Section titled “Internal test timeout”verify-on-stop.sh has its own internal timeout (default 120 seconds) that kills the test runner if it hangs. This is separate from the settings.json timeout. Override via environment variable:
export HOOK_TEST_TIMEOUT=300Matchers
Section titled “Matchers”The matcher field in settings.json is a regex that filters when the hook fires:
| Matcher | Meaning |
|---|---|
"" (empty) |
Fire on all events of that type |
"Edit|Write" |
Fire on Edit or Write tools |
"Edit" |
Fire on Edit tool only |
"Bash" |
Fire on Bash tool only |
Supported Stacks
Section titled “Supported Stacks”verify-on-stop.sh — Test runners
Section titled “verify-on-stop.sh — Test runners”| Stack | Detected By | Command |
|---|---|---|
| Node.js | package.json with scripts.test |
npm/yarn/pnpm/bun test |
| Python | pyproject.toml / setup.py / requirements.txt |
pytest --tb=short -q (via poetry/uv/pipenv/venv/system) |
| Go | go.mod |
go test ./... |
| Java (Maven) | pom.xml |
mvn test -q |
| Java (Gradle) | build.gradle / build.gradle.kts |
./gradlew test |
| Rust | Cargo.toml |
cargo test |
Package manager detection for Node.js: pnpm-lock.yaml → pnpm, yarn.lock → yarn, bun.lockb / bun.lock → bun, otherwise npm.
Python tool detection priority: poetry.lock → poetry run, uv.lock → uv run, .venv/bin/<tool> → direct venv, system PATH → bare command. For type checking, tries mypy first, then ruff check, then pyright. For formatting, tries black first, then ruff format.
verify-after-edit.sh — Type checkers
Section titled “verify-after-edit.sh — Type checkers”| Extensions | Detected By | Command |
|---|---|---|
.ts, .tsx, .js, .jsx |
tsconfig.json |
npx tsc --noEmit |
.py |
poetry/uv/venv/system mypy, ruff, or pyright | mypy <file>, ruff check <file>, or pyright <file> |
.go |
go.mod |
go vet ./... |
.java |
pom.xml or build.gradle |
mvn compile -q or ./gradlew compileJava -q |
.rs |
Cargo.toml |
cargo check |
.kt |
build.gradle / build.gradle.kts |
./gradlew compileKotlin -q |
.cs |
dotnet on PATH |
dotnet build --no-restore -q |
Non-source files (.md, .json, .yaml, .env, etc.) are silently skipped.
Writing Custom Hooks
Section titled “Writing Custom Hooks”Input JSON by event type
Section titled “Input JSON by event type”Stop hook:
{ "session_id": "...", "cwd": "/project/path", "hook_event_name": "Stop", "stop_hook_active": false, "last_assistant_message": "..."}stop_hook_active is true when Claude is already continuing due to a previous Stop hook invocation. Always check this to prevent infinite loops.
PostToolUse hook:
{ "session_id": "...", "cwd": "/project/path", "hook_event_name": "PostToolUse", "tool_name": "Edit", "tool_input": { "file_path": "/project/src/index.ts", "old_string": "...", "new_string": "..." }, "tool_response": { "success": true }}Notification hook:
{ "session_id": "...", "cwd": "/project/path", "hook_event_name": "Notification", "title": "Claude Code", "message": "Waiting for permission...", "notification_type": "permission_prompt"}Notification types: permission_prompt, idle_prompt, auth_success, elicitation_dialog.
Template for a custom hook
Section titled “Template for a custom hook”#!/usr/bin/env bashset -euo pipefail
# Guard: skip if jq is not installedif ! command -v jq &>/dev/null; then exit 0fi
# Read event JSON from stdinINPUT=$(cat)
# Parse fieldsTOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Your logic here...
# Exit 0 to pass, exit 2 to block/feed back errors via stderrexit 0Environment variables
Section titled “Environment variables”| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR |
Absolute path to the project root |
Hook precedence
Section titled “Hook precedence”Both project (.claude/settings.json) and global (~/.claude/settings.json) hooks run. Project hooks do not override global hooks — both execute. Global hooks run first, then project hooks.
Dependencies
Section titled “Dependencies”All hooks require:
- bash — any modern version (4.0+)
- jq — JSON parser
Install jq:
# macOSbrew install jq
# Ubuntu/Debiansudo apt install jq
# Fedora/RHELsudo dnf install jqThe hooks gracefully skip if jq is not available (exit 0 with no action).
verify-on-stop.sh optionally uses timeout (GNU coreutils) for test runner timeouts. On macOS, install via brew install coreutils (provides gtimeout). Without it, the test runner runs without an internal timeout (the settings.json timeout still applies as a hard ceiling).
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
| Hook doesn’t fire | Not wired in settings.json | Check .claude/settings.json has the hook entry |
| “jq not found” in verbose output | jq not installed | Install jq (see Dependencies) |
| Type check runs on .md files | Shouldn’t happen (extension filter) | Check hook version; .md is not in the source extensions list |
| Tests hang forever | No internal timeout + slow suite | Set HOOK_TEST_TIMEOUT=60 or install GNU coreutils for timeout |
| Claude keeps looping on test failures | Tests are genuinely broken | The stop_hook_active guard limits to one retry. If tests still fail, Claude stops. |
| No desktop notification on Linux | notify-send not installed |
Install libnotify-bin (Ubuntu) or libnotify (Fedora) |