Skip to content

Claude Code hooks

Project lifecycle hooks for Claude Code sessions. They mirror Grok agent hooks so both agents run the same quality bar. Hooks live under .claude/hooks/, registered in .claude/settings.json, and are version-controlled with the repo.

Layout

.claude/
  settings.json               # registers PreToolUse + PostToolUse + Stop + PreCompact + SessionStart + SubagentStop hooks
  hooks/
    pre-tool-safety.sh        # PreToolUse (matcher: Bash)
    block-generated-edits.sh  # PreToolUse (matcher: Edit|Write|MultiEdit)
    format-on-edit.sh         # PostToolUse
    post-agent-quality.sh     # Stop
    pre-compact-snapshot.sh   # PreCompact
    post-compact-check.sh     # SessionStart (matcher: compact)
    audit-subagent.sh         # SubagentStop
  logs/
    subagent-audit.jsonl      # local, gitignored — written by audit-subagent.sh

PreToolUse — shell safety and deploy approval

When: a Bash tool call is about to run.

Hard deny (no prompt): these patterns are always blocked via permissionDecision: "deny" and the command does not run:

  • rm -rf / or rm -rf ~ (and similar)
  • git push --force / -f
  • git reset --hard
  • git clean -f / -fd
  • mkfs, dd if=, classic fork-bomb

Explicit permission required: these return permissionDecision: "ask", which escalates to Claude Code's normal Allow/Deny permission prompt instead of running silently:

KindExample commands
Production deploy (Vercel)pnpm run deploy, vercel --prod, vercel deploy …
Nested app / CMS deploye.g. pnpm run studio:deploy, package deploy scripts
Data seed / write scriptse.g. pnpm run *:seed-*, node scripts/seed-…

One-off override: prefix the command with ALLOW_DEPLOY=1 to skip the ask decision for that single command.

Timeout: 15 seconds.

Unlike Grok's equivalent hook (a macOS osascript Allow/Deny dialog, fails closed off-GUI), this one reuses Claude Code's built-in permission-prompt UI via permissionDecision: "ask" — no GUI dependency, works the same over SSH.

Manual equivalent: keep a shared pre-tool-safety.sh pattern list (e.g. under .grok/hooks/bin/) if you want to check a command by hand before running it.

PreToolUse — block generated-file edits

When: an Edit, Write, or MultiEdit tool call is about to run.

Hard deny (no prompt, no override): permissionDecision: "deny" when tool_input.file_path matches paths that should never be hand-edited, for example:

  • Generated type dumps (e.g. from a schema/typegen pipeline)
  • Lockfiles (pnpm-lock.yaml and nested package lockfiles)
  • Build output (.next/**, dist/**, studio/dist/**, …)
  • node_modules/**

These files should never be hand-edited, so unlike the deploy/seed commands above there's no ask escalation or ALLOW_DEPLOY-style override — the underlying command (pnpm install, typegen, a build) is always the right fix.

This complements format-on-edit.sh below, which only skips formatting these same paths; this hook stops the edit from happening at all.

Timeout: 15 seconds.

Claude Code-only — no Grok mirror by default (add one under .grok/hooks/ if Grok sessions need the same protection).

Stop — post-agent quality gate

When: the main agent finishes responding (every turn end — Claude Code's Stop event has no equivalent of Grok's end_turn vs. session-end distinction, so this runs on every stop).

Runs a project script chain, typically:

  1. pnpm run post-agent — Prettier write → ESLint → tsc --noEmit → unit tests
  2. A changed-files dead-code / complexity audit vs merge-base (e.g. Fallow with a known-debt baseline)

On failure: stdout is a JSON {"decision": "block", "reason": "..."} object with truncated log output, which blocks the stop and feeds the failures back to the model so it can fix issues and finish again.

Timeout: 600 seconds (format + lint + types + tests can take a while on a cold cache).

Manual equivalent: run the same scripts locally (pnpm run post-agent and your audit command).

PostToolUse — format on edit

When: an Edit, Write, or MultiEdit tool call completes.

Runs: prettier --write on that file only (via pnpm exec prettier), reading the path from tool_input.file_path.

Skips:

  • Missing paths
  • Non-source extensions
  • Vendor / build / generated paths (same set as the generated-file blocklist)

Timeout: 30 seconds. Failures are non-blocking (stderr note only, does not stop the agent).

PreCompact — pre-compaction snapshot

When: just before context compaction runs, whether triggered manually (/compact) or automatically when the context window fills up.

Runs: git branch --show-current, git status --short, git diff --stat (unstaged + staged), git log -5 --oneline, printed to stdout (capped at ~4000 chars). This has no way to block or edit compaction — it exists purely so the branch, changed files, and recent commits survive into the compacted summary instead of depending on someone having run /handoff first.

Timeout: 15 seconds.

Manual equivalent: the /handoff skill (richer — includes in-progress narrative, not just raw git output), or plain git status --short && git diff --stat && git log -5 --oneline.

SessionStart (compact) — post-compaction check

When: a session resumes immediately after a compaction. No-ops for the other SessionStart sources (startup, resume, clear) — checked via the source field on stdin.

Runs: the same git snapshot as PreCompact, injected via hookSpecificOutput.additionalContext so it lands in context as ground truth to check the just-restored summary against, rather than a bare instruction to go verify. Also capped at ~4000 chars.

Timeout: 15 seconds.

Manual equivalent: run /orient right after a compaction to get the same read-only reality check.

SubagentStop — background subagent audit log

When: a subagent (a Task tool call — e.g. an Agent or Explore invocation) finishes.

Runs: best-effort extraction from the subagent's transcript (transcript_path from stdin), appending one JSON line to .claude/logs/subagent-audit.jsonl (local-only, gitignored — never committed):

  • sessionId — from the hook payload
  • agentName — scanned from the transcript for a subagentType/agentType-shaped field; "unknown" if not found
  • toolsUsed — distinct tool names from tool_use blocks in the transcript
  • summary — the subagent's last assistant text block, truncated to ~500 chars

Schema caveat: Claude Code's SubagentStop payload is documented to carry session_id + transcript_path (same shape as Stop), but whether a subagent name/type is present wasn't always verifiable against the installed CLI. agentName extraction is therefore best-effort, not guaranteed.

Failure posture: always exits 0 — a missing/unreadable transcript, a parse error, or a failed log write never blocks the subagent from finishing.

Timeout: 30 seconds.

Claude Code-only — no Grok mirror by default.

Day-to-day flow

What is not hooked

Same exclusions as Grok hooks — long-running servers (dev, nested app servers, docs dev, test watch), slow/heavy commands (e2e suite, full production build), silent auto-fix on Stop (prefer explicit fixes), and interactive/human-driven flows.

Secret-exposing commands (cat .env, printenv, etc.) aren't covered by the shell-safety pattern list — those are gated separately by permission rules (deny entries in .claude/settings.json). The permission ask entries for vercel * / pnpm run deploy overlap with the hook's own ask decision for the same commands; the hook exists so prefix variations and chained commands (e.g. cd nested-app && pnpm deploy) are still caught, not just the literal permission-rule prefixes.

PreCompact and SessionStart have no Grok mirror — Grok CLI doesn't expose a compaction lifecycle hook. Generated-file block and subagent audit are likewise Claude Code-only unless you add Grok equivalents.

Troubleshooting

SymptomCheck
Hooks never runConfirm .claude/settings.json is present and the session was started in the repo root
Stop always blocksRun post-agent and the audit script locally; fix failures
Format-on-edit no-opFile extension not in allowlist, or path is generated/vendor
Deploy/seed command hangsIt's waiting on the Allow/Deny permission prompt from permissionDecision: "ask"
Edit to generated file deniedExpected — regenerate via the proper script instead of hand-editing
Subagent audit log emptyCheck the gitignored log path under .claude/logs/

Frontend Corner — agent ops, tooling, and decision records