Aller au contenu principal

Hooks (Claude Code 2.1+)

The project includes automatic hooks in .claude/settings.json:

Available Hook Events​

EventTypeDescription
SessionStartcommandTriggered at session startup (matchers: startup, resume, clear, compact)
UserPromptSubmitcommandWhen the user submits a prompt (validation, additional context)
PreToolUsecommand/promptBefore tool execution (matcher: Edit|Write, Bash)
PermissionRequestcommand/promptWhen a permission dialog is shown
PostToolUsecommandAfter successful tool execution
PostToolUseFailurecommandAfter a tool failure
SubagentStartcommandSub-agent startup
SubagentStopcommand/promptEnd of sub-agent execution
Stopcommand/promptWhen Claude finishes responding
StopFailurecommandWhen a turn ends on an API error (rate limit, auth failure) — CLI 2.1.78+
SetupcommandProject initialization (init) and maintenance (maintenance)
NotificationcommandNotifications (permission_prompt, idle_prompt, auth_success, elicitation_dialog)
PreCompactcommandBefore context compaction (matchers: manual, auto)
PostCompactcommandAfter context compaction
SessionEndcommandEnd of session
TeammateIdlecommandWhen a teammate agent becomes idle (Agent Teams)
TaskCreatedcommandWhen a task is created via TaskCreate (CLI 2.1.84+)
TaskCompletedcommandWhen a task is marked completed
WorktreeCreatehttpHook type: "http" invoked on worktree creation, must return hookSpecificOutput.worktreePath (CLI 2.1.84+)
InstructionsLoadedcommandWhen CLAUDE.md and rules are loaded
ElicitationcommandWhen an MCP server requests structured input
ElicitationResultcommandWhen the user responds to an MCP Elicitation
PermissionDeniedcommandAfter a permission denial by the auto mode classifier. Return {retry: true} to retry
CwdChangedcommandWhen the working directory changes
DirectoryAddedcommandAfter /add-dir (or the SDK register_repo_root) registers a new working directory mid-session (CLI 2.1.219+)
FileChangedcommandWhen a file is modified
MessageDisplaycommandLets a hook transform or hide assistant message text as it is displayed (CLI 2.1.152+)

Matcher syntax — exact match on hyphenated identifiers (CLI 2.1.195, June 2026)​

A matcher with a hyphenated identifier (e.g. an agent code-reviewer, a tool mcp__brave-search) now exact-matches — it previously substring-matched, which could fire unintentionally. To match all tools from a hyphenated MCP server, use an explicit pattern: mcp__brave-search__.*. This repo's own matchers (Edit|Write|Bash|MultiEdit|NotebookEdit + event names) contain no hyphens, so they are unaffected — but a downstream project matching hyphenated agent/MCP names should use the .* form.

Hook Types​

TypeDescription
commandExecutes a bash script (deterministic, fast)
promptEvaluated via a Haiku LLM (contextual, intelligent) - for Stop, SubagentStop, PreToolUse
httpSends a JSON POST to a URL (external webhook) - CLI 2.1.70+

Hook Properties​

PropertyDescription
asynctrue to run in the background without blocking (CLI 2.1.70+)
timeoutTimeout in seconds (not milliseconds: measured, timeout: 3 lets a 1 s hook finish and timeout: 2 cuts a 4 s one). A PreToolUse guard that times out does not block unless it carries onFailure: "block" (below). Give blocking guards a wide budget either way (the foundation uses 30 s, 1800 s for test/CI gates): without block a cut guard is a silent bypass, with it a cut guard refuses a legitimate call. tests/settings-guards.bats enforces both. Not enforced on async: true command hooks
onFailure"block" or "continue" (default), on command and http hooks, Claude Code 2.1.295+. With "block", a hook that cannot start, times out, exits with a code other than 0 or 2, or returns invalid JSON blocks the action instead of letting it through; no effect on async, Stop and SubagentStop hooks. Every foundation PreToolUse guard carries "block", so a guard that crashes, is cut or is missing fails closed, except the zsh PIPESTATUS guard: a correctness check, it carries an explicit "continue" so its failure never refuses every Bash call (tests/settings-guards.bats lists the exceptions); its deliberate fail-open paths (no jq, a missing policy core) exit 0 and are unchanged. An older CLI ignores the field (measured on 2.1.292). ⚠️ Any other value ("ignore", which pre-#587 installs carry) on one PreToolUse hook silently disables every PreToolUse guard (measured 2026-10-17 on 2.1.302); claude-base doctor reports it as hook-onfailure-invalid
ifActivation condition using permission rules syntax (CLI 2.1.90+)
additionalContextAdditional context string injected into the PreToolUse hook (CLI 2.1.110+)

defer permission (PreToolUse)​

PreToolUse hooks can return "defer" as a permission decision. The headless session pauses at the tool call and can resume with -p --resume to re-evaluate the hook. Useful for CI/CD workflows requiring human approval.

MCP transient retry (CLI 2.1.128+)​

When a hook interacts with an MCP server, transient connection failures are auto-retried by the runtime. Hook authors do not need to wrap MCP calls in custom retry logic for transient cases (the typical "server momentarily unavailable" pattern). Permanent failures are not retried.

terminalSequence field (CLI 2.1.141+)​

Hook JSON output gained a terminalSequence field that emits raw terminal escape sequences — desktop notifications, window-title updates, and the bell — without requiring a controlling terminal. Useful for background or daemonized hooks that want to surface status outside the Claude Code TUI. See the upstream changelog for the exact escape codes accepted.

The exact retry bound and the failure-classification heuristics are tuned upstream and may evolve between releases — refer to the Claude Code changelog for the canonical behavior at the version you target.

Configured Hooks​

HookTriggerAction
Session infoSessionStart (startup)Displays project information at startup
Check node_modulesSessionStart (startup)Checks that node_modules exists if package.json is present
Git hooks wiringSessionStartRepairs a broken core.hooksPath so the committed .husky/ pre-commit (the counts self-heal) actually runs. Covers the two breakages that happen after init and so escape setup-deps.sh: a fresh clone (local config is not cloned, .husky/ arrives without its wiring) and a repo rename (a stale absolute path silently disables every hook). Silent unless it repairs, and it never takes over a hooksPath that points somewhere real — a deliberate .githooks is left alone (scripts/hooks/git-hooks-wire.sh)
Scratchpad sizeSessionStartClaude Code gives every session a temp dir (its scratchpad) and nothing removes it when the session ends; where /tmp is a tmpfs that is RAM (measured 2026-09-24: one session left 4 GB of node_modules there and the OOM killer ended another session). When the whole tree passes CLAUDE_BASE_SCRATCH_WARN_MB (default 1024) it prints the total, the three largest other sessions with their rebuildable dirs, and a tmpfs note; it always reports files you do not own (a docker run -v without --user). A scan that outlives its budget (CLAUDE_BASE_SCRATCH_SCAN_SECONDS, default 5, where timeout exists) is reported as too large to measure, never skipped. Runs on startup only, not on each compaction. Reports only, never deletes. To move the tree to disk, set CLAUDE_CODE_TMPDIR in ~/.claude/settings.json env — project-level env cannot set it since Claude Code 2.1.251 (scripts/hooks/scratchpad-check.sh)
Main protectionPreToolUse (Edit/Write)Keeps edits off main/master by auto-creating a feature/auto-<timestamp> branch first; blocks the edit (exit 2) if the branch can't be created, instead of letting it land on main. Scoped to the target path: an edit aimed outside the working tree (a dotfile in $HOME, another project) is left alone, so it no longer strands an empty feature/auto-… behind it — an unreadable target (no jq, no path) still guards. Disable with ALLOW_MAIN_EDIT=1 (scripts/hooks/main-branch-guard.sh)
Secrets detectionPreToolUse (Write/Edit/MultiEdit)Built-in zero-dependency scan blocks hardcoded provider secrets (AWS/Stripe/GitHub/Slack/Google/PEM) before writing; also runs gitleaks if installed. Placeholders ignored. Disable with SKIP_SECRET_SCAN=1 (scripts/hooks/secret-scan.sh)
Destructive migrationPreToolUse (Write/Edit/MultiEdit)Confirm+backup reminder before destructive DDL (DROP/TRUNCATE) in a migration file — closes the gap the Bash-only destructive guard misses. Disable with SKIP_DESTRUCTIVE_CHECK=1 (scripts/hooks/destructive-migration.sh)
Pre-commit testsPreToolUse (Bash git commit)Runs the stack's test suite (npm/pytest/go) before a commit and blocks (exit 2) on failure; npm's npm init -y placeholder test script (always exits 1, no pretest/posttest) is skipped with a notice, not run, and the pytest / go test checks still run; detects/repairs Husky if needed. npm runs on the project's own Node: through mise exec when mise and a mise.toml / .mise.toml / .tool-versions are present (falling back, with a notice, if mise refuses an untrusted config); if the Node it can reach is still below engines.node, the npm suite is skipped with the reason, never run on the wrong runtime (scripts/hooks/_node-runtime.sh). Disable with SKIP_PRE_COMMIT_TESTS=1 set in the environment Claude Code runs in (a prefix on the command does not reach the hook) (scripts/hooks/pre-commit-tests.sh)
Local pre-push CIPreToolUse (Bash git push)Lint + type-check + tests before push; npm's placeholder test script is skipped with a notice, and a package.json holding only that placeholder does not stop the Python / Go checks. npm runs on the project's own Node (mise, engines.node), as for the pre-commit tests above (scripts/hooks/_node-runtime.sh). Disable with SKIP_PRE_PUSH_CI=1 set in the environment Claude Code runs in (scripts/hooks/pre-push-ci.sh)
Pre-deploy buildPreToolUse (Bash deploy)Runs the production build (npm run build / go build) before a deploy command and blocks (exit 2) on failure — including a broken Go build. Disable with SKIP_PRE_DEPLOY_BUILD=1 (scripts/hooks/pre-deploy-build.sh)
Destructive ops guardPreToolUse (Bash)Blocks destructive DB/filesystem ops — DROP/TRUNCATE, an unscoped DELETE FROM (no WHERE), wiping an uploads/media/storage tree, prisma migrate reset. Disable with SKIP_DESTRUCTIVE_CHECK=1 (scripts/hooks/destructive-ops.sh)
Command validatorPreToolUse (Bash)Validates commands against 9 risk categories (fork bombs, pipe-to-shell, disk destruction, privilege escalation, deleting a whole home directory whether spelled as a path, ~ or $HOME, git --no-verify / commit -n gate-bypass, etc.). Disable all with SKIP_COMMAND_VALIDATOR=1, or just the no-verify block with SKIP_NO_VERIFY_CHECK=1
Config protectionPreToolUse (Edit/Write/MultiEdit)Blocks modifying an existing linter/formatter config (.eslintrc*, eslint.config.*, .prettierrc*, biome.json, ruff.toml, .markdownlint.*) so a failing check is fixed in the code, not silenced. Creating a config is allowed; pyproject.toml/tsconfig.json and test fixtures are exempt. Disable with SKIP_CONFIG_PROTECTION=1
Bash-write guardPreToolUse (Bash)The Bash-side complement to the guards above: blocks a Bash write (>, >>, tee, sed -i) whose target is an existing lint/format config, an existing secrets file (.env, *.pem, *.key, credentials, …), or a tracked repo file while on main/master — closing the gap where a write routed through Bash escaped config-protection/secret-scan/main-branch-guard. New files, temp paths and /dev/* pass. Shares the config set via _sensitive-paths.sh. Disable with SKIP_BASH_WRITE_GUARD=1 (or ALLOW_MAIN_EDIT=1 for the on-main case)
zsh PIPESTATUS guardPreToolUse (Bash)Blocks a command that reads PIPESTATUS when the Bash tool runs it under zsh (CLAUDE_CODE_SHELL, else SHELL): zsh has no such array and expands it to an empty string with no error, so a failed pipe stage reads as success. The message points to zsh's ${pipestatus[1]} (1-indexed), bash -c '…' or set -o pipefail. Only an expansion zsh performs counts: single quotes, \$, the bare word, comments and a quoted-delimiter heredoc body (<<'EOF', writing a script) pass. Silent when the shell is bash. Fails open (onFailure: "continue"): a crash or a missing script lets the call through. Disable with SKIP_ZSH_PIPESTATUS_GUARD=1 (scripts/hooks/zsh-pipestatus-guard.sh)
Auto-format TS/JSPostToolUse (Edit/Write)Prettier on TS/JS files
Auto-format PythonPostToolUse (Edit/Write)Ruff/Black on .py files
Auto-format GoPostToolUse (Edit/Write)gofmt on .go files
Auto-format RustPostToolUse (Edit/Write)rustfmt on .rs files
Auto-format DartPostToolUse (Edit/Write)dart format on .dart files
Auto-format LuaPostToolUse (Edit/Write)stylua on .lua files
Inline edit errors (output rewriter)PostToolUse (Edit/Write)Runs tsc + eslint on edited TS/JS files and appends errors to the tool result envelope (CLI 2.1.121+). Disable: SKIP_INLINE_EDIT_ERRORS=1. Replaces the former inline tsc + eslint blocks.
Auto-installPostToolUse (Edit package.json)npm/yarn/pnpm/bun install
Auto-sync PythonPostToolUse (Edit pyproject.toml)uv sync or pip install
Auto pub getPostToolUse (Edit pubspec.yaml)flutter/dart pub get
Auto go mod tidyPostToolUse (Edit go.mod)go mod tidy
Auto cargo checkPostToolUse (Edit Cargo.toml)cargo check
Coverage checkPostToolUse (Edit test files)Checks test coverage
Substance gatePostToolUse (Edit/Write/MultiEdit)Advisory: runs scripts/substance-check.sh on the edited test/source file and surfaces hollow tests (no-assertion / always-true / skipped / empty) and implementation stubs (not implemented/NotImplementedError/panic("TODO")) that coverage % misses. Never blocks (exit 0 always). Disable: SKIP_SUBSTANCE_CHECK=1
Base integrity check (base-integrity-check.sh)PostToolUse (Edit/Write/NotebookEdit)Advisory, foundation-repo only: when the edited file lives in .claude/skills/agents/commands/rules/ or is .claude/settings.json, runs validate-counts.sh in warning mode as a reminder to sync counters / catalog / SessionStart message. Never blocks. Disable: SKIP_BASE_INTEGRITY=1
Setup initSetup (init)Installs dependencies on first run
Setup maintenanceSetup (maintenance)Periodic audit and updates
Lifecycle logs (log-event.sh)Notification, SubagentStop, TeammateIdle, TaskCompleted, SessionEnd, PreCompact, PostCompact, InstructionsLoaded, Elicitation, ElicitationResult, PermissionDenied, UserPromptSubmit, PostToolUseFailureOne line per event in ~/.local/state/claude-base/<log>.log ($XDG_STATE_HOME honoured, CLAUDE_BASE_LOG_DIR overrides), directory 700 and files 600. Records a tag, a UTC timestamp and named scalar fields only (tool_name, notification_type, agent_type, reason, trigger, mcp_server_name) — never the payload's text, so a refused command or a transcript path cannot land in a log. Replaces fourteen inline hooks that wrote to world-readable /tmp
Prompt context injectionUserPromptSubmitInjects branch, modified files, LOC diff and /assistant-auto hint if no slash command (disable: SKIP_PROMPT_CONTEXT=1). Also injects a once-per-session vendor-precedence hint when a graduated vendor skill is installed (prefer it over the foundation pointer, vendor-precedence T3; pure-shell helper _vendor-precedence-hint.sh, disable: SKIP_VENDOR_PRECEDENCE=1)
Check .envSessionStartChecks that .env is in .gitignore
Third-party hooks warningSessionStartWarns if custom hooks are detected
CLI version probeSessionStartProbes Claude Code version for the output rewriter (requires 2.1.121+). Writes /tmp/claude-rewriter-supported (1 or 0) consumed by post-edit and bash-output rewriter hooks

Core/shell split (policy libs)​

The guard hooks are split into a harness-neutral policy core and a thin Claude Code shell (see specs/agnostic-core/): the decision logic (pattern tables, verdicts) lives in sourceable scripts/hooks/_core-helpers.sh and scripts/hooks/_policy-*.sh libs — plain string in, data verdict out (return 0 allow / 1 deny + reason on stdout) — while the hook script itself only reads the stdin envelope and translates a deny into stderr + exit 2. The policy libs are directly tested by tests/policy-*.bats (no envelope), the hook scripts by their existing contract suites; tests/policy-structure.bats enforces the split (core purity, thin shells, manifest closure) and specs/agnostic-core/portability-map.md classifies every hook file. Do not register a _*.sh lib in settings.json — they are not hooks.

Git-native hooks (.husky/)​

Distinct from the Claude Code hooks above (which live in .claude/settings.json): these are standard git hooks, wired via core.hooksPath=.husky. They run for every commit — by a human or an agent, in any tool — not only inside Claude Code. scripts/hooks/git-hooks-wire.sh keeps core.hooksPath wired: on Setup (via setup-deps.sh) and on every SessionStart, because the two ways the wiring breaks — a fresh clone, which does not carry local config, and a repo rename leaving a stale absolute path — both happen after init. It is idempotent and silent unless it repairs. A manual git config core.hooksPath .husky is no longer needed after a clone; outside Claude Code, run the script directly.

HookAction
pre-commit → counts self-healWhen a commit stages a counted artifact (.claude/{commands,agents,skills,rules,presets}/**, docs/**, specs/marketplace-audit/**, the minimal manifest or VERSION), runs scripts/sync-counts.sh: regenerates the derived count files and git adds them, so a stale count can never reach CI. No-op (and node-free) for any other commit. Blocks only if counts drifted and regeneration is unavailable. Disable with SKIP_COUNTS_SYNC=1.
pre-push → preflightRuns the foundation's own CI gates locally before a push (scripts/preflight.sh --fast: shellcheck · validate-counts.sh · manifest-hooks-coverage), so failures surface locally instead of post-push. Mirrors ci.yml; scripts/preflight.sh --full adds the complete bats suite. A gate whose tool is absent does not run: it prints SKIPPED on its own line and the run withholds the "all gates passed" line (non-blocking — CI's Linux job is authoritative). Bypass once with SKIP_PREFLIGHT=1.

scripts/sync-counts.sh mirrors the CI "Counts gate": it regenerates and stages the same derived path set the CI diffs (counts.json, README.md, CLAUDE.md, docs/, website/docs/, the Docusaurus config). --check runs the read-only validate-counts.sh instead (no regeneration, no staging).

Output rewriter (CLI 2.1.121+)​

Three coordinated hooks that exploit hookSpecificOutput.updatedToolOutput to tighten Claude's feedback loop on PostToolUse Bash and Edit/Write events.

HookEventRole
check-cli-version.shSessionStartProbes claude --version and writes /tmp/claude-rewriter-supported (1 if >= 2.1.121, 0 otherwise). On unsupported CLI, prints a one-line notice.
bash-output-filter.shPostToolUse (Bash)Trims allowlisted noisy command outputs (npm/pnpm/yarn/bun install/audit/test/build, pytest, go test/build, cargo build/test/check) to actionable lines. Outputs below 30 lines pass through unchanged.
post-edit-typecheck-and-lint.shPostToolUse (Edit\Write)

All three hooks bail out silently if the sentinel reports unsupported, if jq is absent, or if their respective opt-out env var is set. The Bash filter and inline-edit hook share the helpers in _hook-helpers.sh (sourced, not registered).

Migration path: existing projects must run claude-base update -f --all <project> to get the consolidated .claude/settings.json. If only --hook-scripts ran, post-edit-typecheck-and-lint.sh will detect the legacy state at runtime (old npx tsc --noEmit references in .claude/settings.json) and emit a one-line notice once per session.

Hook Environment Variables​

VariableUsage
ALLOW_MAIN_EDIT=1Disable main branch protection
SKIP_PRE_COMMIT_TESTS=1Disable pre-commit tests
SKIP_COUNTS_SYNC=1Disable the git pre-commit counts self-heal (.husky/pre-commit)
SKIP_PREFLIGHT=1Skip the git pre-push foundation gates (.husky/pre-push → scripts/preflight.sh)
SKIP_COMMAND_VALIDATOR=1Disable ALL command security validation (every category)
SKIP_NO_VERIFY_CHECK=1Disable ONLY the git --no-verify/-n gate-bypass block (keeps the other 8 categories active)
SKIP_CONFIG_PROTECTION=1Disable the linter/formatter config-protection hook
SKIP_SECRET_SCAN=1Disable the built-in hardcoded-secret scan (scripts/hooks/secret-scan.sh)
SKIP_SUBSTANCE_CHECK=1Disable the substance gate (advisory hollow-test / stub detector)
SKIP_PRE_PUSH_CI=1Disable local pre-push CI check
CLAUDE_BASE_GATE_SECONDS=NTime budget, shared across all steps, of each blocking gate (pre-commit tests, pre-push CI, pre-deploy build); default 1500, below their 1800 s hook timeout. When it runs out the gate blocks (exit 2) with its own message, before the hook timeout cuts it (a cut gate lets the action through on a CLI older than 2.1.295, and is refused with a generic harness error from 2.1.295 under onFailure: "block"). Needs timeout or gtimeout; without either the gate runs unbounded (scripts/hooks/_gate-budget.sh)
SKIP_DESTRUCTIVE_CHECK=1Disable destructive operations protection
SKIP_PROMPT_CONTEXT=1Disable repo context injection on free-form prompts
SKIP_VENDOR_PRECEDENCE=1Disable the once-per-session installed-vendor precedence hint
SKIP_BASH_OUTPUT_FILTER=1Disable the Bash output filter (output rewriter)
SKIP_INLINE_EDIT_ERRORS=1Disable the inline edit errors hook (output rewriter)
BASH_OUTPUT_FILTER_VERBOSE=1Keep both filtered and original views in the rewritten output
BASH_OUTPUT_FILTER_THRESHOLD=<N>Override the noise threshold (default 30 lines) below which Bash outputs pass through unchanged
HOOK_REWRITER_SENTINEL=<path>Override the capability sentinel path (default /tmp/claude-rewriter-supported). Used by tests to isolate parallel runs under $BATS_TEST_TMPDIR
HOOK_REWRITER_METRIC_LOG=<path>Override the bash filter metric log path (default ~/.local/state/claude-base/rewriter.log, created 600: it holds the start of each command). Same testing rationale
HOOK_LEGACY_NOTICE_SENTINEL=<path>Override the legacy notice sentinel base path (default /tmp/claude-base-legacy-warned, suffixed with .PPID). Same testing rationale

Log Files​

The lifecycle logging hooks (log-event.sh) and the output rewriter append to ~/.local/state/claude-base/ ($XDG_STATE_HOME/claude-base/ when set; override with CLAUDE_BASE_LOG_DIR). The directory is 700 and each file 600. A line holds a UTC timestamp, a tag and named scalar fields, never the payload's text. Earlier versions wrote /tmp/claude-*.log, readable by every account on the machine: delete those.

FileContent
sessions.logInstructions loaded, compaction (trigger), end of session (reason), tasks completed
agents.logSub-agent stops (agent_type) and idle teammates
notifications.logPermission prompts and user waits (notification_type)
mcp.logMCP Elicitation requests and results (mcp_server_name)
permissions.logPermissions denied by the auto mode classifier (tool_name)
prompts.logUser prompt submissions (timestamps only)
failures.logTool failures (tool_name)
rewriter.logOutput rewriter activity (start of the command, original / filtered line counts)