Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when configuring Claude Code hooks in .claude/settings.json: the exact event-name casing, matcher/array nesting, command fields, timeout unit, and exit-code / permission-decision semantics — which cheaper models do not emit by default.
.claude/skills/claude-code-hooks-config/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 94% | 34 |
| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +32% | +83% | 0% | 22 | 54d ago |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | — | — |
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-06 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
Claude Code hooks are declared in .claude/settings.json under one top-level hooks object that maps a lifecycle EVENT to matcher groups, each running shell commands. Apply this whenever wiring automation (format, lint, test, guard, notify) to Claude's tool use or session lifecycle. The shape is fixed and non-obvious; guessing the field names, casing, or exit-code meaning produces config that silently never fires.
hooks (lowercase) in.claude/settings.json.
hooks, each key is an event name in exactPascalCase — no lowercasing, no snake_case, no invented on…/before…/after… names. The common events:
PreToolUse — before a tool runs (can block it)PostToolUse — after a tool completesUserPromptSubmit — when you submit a messageSessionStart / SessionEnd — session begins / endsStop — the main agent finishes respondingSubagentStop — a spawned sub-agent finishesNotification — Claude Code emits a notification (e.g. awaiting permission)PreCompact — before history compactionevent straight to one handler object, and never key it by tool name.
{ "matcher": …, "hooks": [ … ] }.matcher is a string. For PreToolUse / PostToolUse it matches the tool name — Bash, Edit,Write, MultiEdit, Read, Grep, Task, an mcp__* server tool, etc. It is matched as an unanchored regex, so Edit|Write covers several tools. It is not a file path or a glob like *.py.
"*", "", or by omitting matcher.SessionStart, SessionEnd, PreCompact, …) do not match tool names; a plaingroup without a tool matcher is fine.
hooks (the same word, one level deeper) — not handlers,actions, commands, or steps. It holds one or more handler objects, run in order.
{ "type": "command", "command": "<shell>" }. The type is the literal string"command" (not shell, exec, script, or absent). The shell string goes under command (not run, cmd, exec, or script).
$CLAUDE_PROJECT_DIR. To call a repo script from a command, prefix itwith $CLAUDE_PROJECT_DIR (or ${CLAUDE_PROJECT_DIR}), e.g. "$CLAUDE_PROJECT_DIR/scripts/fmt.sh". Never hardcode an absolute machine path, and don't rely on $PWD — hooks may run from a nested directory.
timeout is in SECONDS. The optional per-handler timeout is a number of seconds (e.g. 60), nevermilliseconds. "timeout": 5000 means 83 minutes, not 5 seconds.
0 = success (stdoutmay hold JSON control); 2 = blocking error — stderr is fed back to Claude and, for PreToolUse, the tool call is blocked; any other non-zero code = non-blocking error (surfaced but execution continues). It is exit code 2, not 1, that blocks.
PreToolUse control, print JSON with ahookSpecificOutput object: "hookEventName": "PreToolUse" plus "permissionDecision" set to one of "allow", "deny", or "ask", with a "permissionDecisionReason" string. Post-run events use a top-level "decision": "block" + "reason". There is no bare top-level block: true / allow: false.
Format after every edit (the base default vs. the real schema)
BEFORE (invented, will not fire):
json{ "hooks": { "afterEdit": { "run": "prettier --write ${file}" } } }
AFTER:
json{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "prettier --write \"$CLAUDE_PROJECT_DIR\"" } ] } ] } }
Guard shell commands — block on rejection:
BEFORE:
json{ "hooks": { "preBash": { "command": "guard.sh", "blockOnExit": 1 } } }
AFTER (exit 2 blocks; project-root script):
json{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/guard.sh" } ] } ] } }
Run a script at session start:
BEFORE: { "hooks": { "onStart": "notes.sh" } } AFTER:
json{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/notes.sh" } ] } ] } }
Timeout unit: BEFORE "timeout": 60000 → AFTER "timeout": 60 (seconds).
"matcher": "*", "matcher": "", or omit matcher entirely — do not enumerate every tool.matcher, e.g. "Edit|MultiEdit|Write".matcher cannot select .go/.py files — it only matches the tool. Do theextension check inside the command (read the changed path from the hook's JSON stdin).
hooks array; they run in order.SessionStart can match startup|resume|clear|compact; most hooks just omit it.PostToolUse). DON'T invent afterEdit, on_edit, posttooluse.hooks. DON'T use handlers, actions, or commands."type": "command". DON'T omit type or use shell/exec.command. DON'T use run/cmd/script.matcher to a tool name. DON'T set it to a file glob or a path.2 to block. DON'T assume exit 1 blocks.$CLAUDE_PROJECT_DIR. DON'T hardcode absolute paths or trust $PWD.timeout in seconds. DON'T use milliseconds.preToolUse, post_tool_use) — the event never matches.handlers / actions instead of the nested hooks array.matcher set to a file glob (*.js) instead of the tool name.type omitted, or run/cmd used instead of command.1 used to block (only 2 blocks; 1 is a non-blocking error).timeout given in milliseconds.{ "block": true } instead of hookSpecificOutput.permissionDecision.$CLAUDE_PROJECT_DIR.hooks object in .claude/settings.json.{ matcher, hooks:[…] } groups.matcher = tool name / regex / * (not a file glob).hooks."type": "command" + command.$CLAUDE_PROJECT_DIR; timeout in seconds.2, or JSON permissionDecision allow|deny|ask.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/10/2026 | +88% |
Other measured skills in the registry, with their headline benchmark lift.