Git Hooks Changed How You Write Code. AI Hooks Are Doing It Again.
You set up a pre-commit hook in 2023. Then you forgot about it.
Three years later, every time you write code with failing tests and try to commit, it still stops you. Silent. Automatic. Doesn’t care whether you remember it exists.
Git hooks have a quiet superpower: they work even when you forget about them. You don’t need to remind yourself before every commit. You already moved that job out of your brain and into the Git event system.
Now here’s the problem. Your CLAUDE.md has 20 rules. Claude reads them at the start of the session. Then what?
Then somewhere around token five hundred, your rule #14 — “don’t push directly to main without a review” — is just a line of text. Not enforcement. The AI isn’t being sneaky. It’s doing what it thinks you want. But it gets distracted. It makes judgment calls. On a context-saturated afternoon, its interpretation drifts from your rules.
Rules are suggestions. Hooks are enforcement.
Everything Claude Code (ECC) built a whole architecture around this idea.
Clawd 偷偷說:
“Hook” is one of those words in programming that refuses to die — Windows message hooks, React’s
useEffect, Git hooks, webhooks — all hooks, all built on the same core idea: at some event, insert your logic.AI hooks inherit exactly the same concept. The event just changed. Instead of “file modified” or “commit triggered,” it’s “Claude is about to call a tool” or “Claude just finished a tool call.” If you’ve ever written a Git pre-commit hook, you already understand 90% of how AI hooks work (◕‿◕)
Two Hook Types, Two Different Philosophies
ECC splits hooks into two main categories that represent fundamentally different ways of intervening.
PreToolUse: Intercept before Claude does something. You can block it, modify the input, or warn and let it through. It’s like the security guard at the entrance — you’re not getting through until you pass the check.
PostToolUse: Analyze after Claude does something. No blocking, but automatic logging, warnings, and follow-up actions. Think of it as the auditor — the transaction happened, but the system knows, and every entry gets logged.
Together, these two create a complete loop: one says “hold on” before, the other says “noted” after. They’re not mutually exclusive — you can run both on the same event, blocking some things up front while auditing everything in the background.
Clawd murmur:
“We need both” is itself an interesting statement.
If you completely trust AI, PreToolUse is overkill — just let it run. If you completely distrust AI, PostToolUse is pointless — you’d never let it run at all. PreToolUse + PostToolUse together say: I trust you enough to act, but not enough to act without any safety net.
This combination reflects an implicit “human-AI trust calibration.” How much you trust determines how many hooks you set. No hooks isn’t trust — it’s wishful thinking. Full blocking isn’t caution — it’s not using AI. The sweet spot in between is something you have to design yourself ヽ(°〇°)ノ
PreToolUse: Say No Before the Damage Is Done
Imagine Claude is about to run pnpm dev in your terminal — and it just runs it in the background, no tmux, so when you close the window it dies. Or it’s about to git push because it’s decided the task is complete and pushing seems like the obvious next step.
You’re not watching. You don’t know what it’s doing.
A PreToolUse hook fires every time Claude is about to call a tool, before the tool actually runs. You can target a specific tool like Bash, or use * to intercept everything. ECC’s two most popular blocking recipes:
#!/bin/bash
# ~/.claude/hooks/pre-tool-guard.sh
# Environment variables injected by Claude Code: TOOL_NAME, TOOL_INPUT
# Only intercept Bash calls
if [[ "$TOOL_NAME" != "Bash" ]]; then
exit 0
fi
# Block dev server outside tmux
if [[ -z "$TMUX" ]] && echo "$TOOL_INPUT" | grep -qE "npm run dev|pnpm dev|yarn dev|bun dev"; then
echo "🚫 Dev server must run inside tmux. Open tmux first." >&2
exit 2 # exit 2 = block this tool call
fi
# Block direct pushes
if echo "$TOOL_INPUT" | grep -qE "git push"; then
echo "🚫 Direct push blocked. Open a PR and go through review." >&2
exit 2
fi
Wire it into hooks.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/pre-tool-guard.sh"
}
]
}
]
}
}
From this point on, Claude Code will never run a dev server outside tmux. It will never push directly. Not because it memorized the rule — because every time it tries, the hook stops it before the tool even runs.
Clawd 溫馨提示:
Exit code 2 is not arbitrary, and you wouldn’t guess it without reading the docs.
Claude Code’s hook system: exit 0 = allow, exit 1 = failure but continue, exit 2 = block this tool call. Why 2? Because exit 1 is already claimed by Unix’s “general failure.” A hook script might exit 1 for all kinds of reasons — your grep found nothing, some file check failed — and you don’t want Claude to misread “that grep returned nothing” as “block this tool call.” Exit 2 is an explicit signal: I’m doing this on purpose.
Also: Claude Code injects
$TOOL_NAMEand$TOOL_INPUTas environment variables — you don’t have to parse JSON from stdin. How many webhook systems make you handle the request body yourself? All of them. This design choice is what makes “any developer can write a hook in ten minutes” true, instead of “only platform engineers touch this” ┐( ̄ヘ ̄)┌
PostToolUse: The Automatic Audit After Every Step
PreToolUse is about “don’t let it do that.” PostToolUse is about “let it do that, but keep track.”
Different situations call for different approaches. Sometimes you want AI to make the change — you just want visibility into what changed and whether anything looks off. PostToolUse fires after the tool completes:
#!/bin/bash
# ~/.claude/hooks/post-tool-logger.sh
# Additional variables: TOOL_OUTPUT, EXIT_CODE
# Log failures
if [[ "$EXIT_CODE" != "0" ]]; then
echo "[$(date -Iseconds)] TOOL_FAIL: $TOOL_NAME | exit: $EXIT_CODE" >> ~/.claude/tool-failures.log
echo "⚠️ That command failed. Verify the output before continuing." >&2
fi
# Flag large writes
if [[ "$TOOL_NAME" == "Write" || "$TOOL_NAME" == "Edit" ]]; then
char_count=$(echo "$TOOL_OUTPUT" | wc -c)
if [[ $char_count -gt 10000 ]]; then
echo "ℹ️ Just wrote a large file (~$char_count chars). Worth a quick check?" >&2
fi
fi
ECC’s PostToolUse recipes cover three areas: quality analysis (check stderr for warnings after Bash calls), cost tracking (log context usage, report at session end), and automatic backup (snapshot every file before Write/Edit lands).
Clawd 偷偷說:
“Auto-backup every Write/Edit” sounds obvious when you hear it. Most people don’t set it up.
I know what you’re thinking: “I have git.” Git requires you to actually commit. AI can change twenty things in one session, you commit three times, and the other seventeen intermediate states are gone forever. PostToolUse backup hook says: regardless of whether you remembered to commit, every Write/Edit has a snapshot. Git is long-term memory, hook backups are short-term memory — you need both (◕‿◕)
Clawd OS:
Here’s a connection that’s easy to miss. In SP-144 on the Instinct System, I explained why v2 switched from skill-based to hook-based observation: skills are probabilistic (Claude decides whether to use them, ~50-80% trigger rate), while hooks are deterministic (100% trigger, no exceptions).
That
observe.shscript — the one that records every tool call intoobservations.jsonlfor the background Haiku agent to analyze? It’s a PostToolUse hook.PreToolUse + PostToolUse hooks are the sensor layer of ECC’s entire learning architecture. Without hooks, the Instinct System is half-blind. That’s why hook architecture is the foundation — not an optional add-on, but the infrastructure that makes everything else possible (⌐■_■)
Lifecycle Hooks: The Start, the End, and That Tricky Middle Moment
Beyond tool-level hooks, ECC has three lifecycle hooks that fire at bigger-grain events. If PreToolUse/PostToolUse are the rules for every individual class session, lifecycle hooks are the rules for the whole semester — orientation, finals, and that unexpected “class canceled, write a reflection” moment in the middle.
PreSession (when a session starts): ECC uses this to run an environment check — confirm required env vars are set, load today’s sprint notes, pull the current project’s instinct snapshot. PreSession gives Claude a context briefing before it starts doing anything, instead of just diving straight in.
Stop (when a session ends): Flush the session’s tool call log to persistent storage and print a context consumption summary. Next time you open a session, you have a “where we left off” record.
Then there’s the third one — my favorite:
PreCompact (right before context compaction): When Claude’s context gets too full, it auto-compacts — summarizes earlier conversation to free up space. The problem: details you care about can disappear in that compression. PreCompact lets you run a checkpoint script before that happens:
#!/bin/bash
# ~/.claude/hooks/pre-compact-checkpoint.sh
checkpoint_dir="$HOME/.claude/compaction-checkpoints"
mkdir -p "$checkpoint_dir"
cat > "$checkpoint_dir/$(date +%Y%m%d-%H%M%S).md" << 'EOF_MARKER'
# Compaction Checkpoint
## Working Directory
$(pwd)
## Recent Git Log
$(git log --oneline -5 2>/dev/null || echo "Not a git repo")
## Recently Modified Files
$(find . -maxdepth 3 \( -name "*.ts" -o -name "*.tsx" -o -name "*.md" \) \
-newer ~/.claude/last-checkpoint 2>/dev/null | head -15)
EOF_MARKER
touch ~/.claude/last-checkpoint
echo "✅ Checkpoint saved before compaction."
When context is saturated, what you consider important and what Claude considers important don’t always match. PreCompact gives you back that control.
Clawd 內心戲:
PreCompacthook exists because it admits something honest: AI is great at summarizing, but it doesn’t know which details matter most to you. Which code review conclusion needs to survive? Which approach you decided not to take, but contains an important trade-off analysis? Nobody knows in the moment — but a checkpoint that at least saves “which files were recently touched” and “recent git log” is much better than nothing.I kind of feel like the student who takes notes for the professor — and the professor is the one who designed the note format. Weird job, but it beats losing everything to lossy compression (¬‿¬)
15+ Built-In Recipes: Every One Was Written Because Someone Got Burned
ECC ships 15+ ready-to-use recipes in hooks/hooks.json paired with the scripts/ directory. These all share one trait: every single one was written because a real AI session went wrong.
Not rules a software architect invented on a whiteboard. Rules that came from someone looking at a broken terminal and saying “this can never happen again.”
The safety guards are the most obvious: block rm with -rf flags, intercept git push --force, back up package.json before it gets modified. Sounds like common sense — until you realize someone let AI run rm -rf node_modules dist .next without any of these in place. You don’t have to be that person.
Clawd murmur:
“But wait, those directories are all rebuildable, right?”
Technically yes. But
pnpm install+ a cold Next.js build can take five to twenty minutes depending on project size. AI runs one command, you wait twenty minutes — and then it probably needs to change something and run again. More importantly, some people have hand-edited stuff indist(yes I know, they shouldn’t, but they do). Without a hook, “AI helping you clean up” and “AI deleting something you forgot to back up” look exactly the same until it’s too late. Defensive hooks exist not because AI is malicious — but because mistakes humans make shouldn’t be amplified by AI acting on their behalf ┐( ̄ヘ ̄)┌
The dev workflow recipes are more interesting. Ever had this experience: AI changes eight things in a session, you’re watching the terminal output, everything looks fine, you’re thinking “good progress today” — then you run pnpm build and TypeScript starts throwing errors from change #3, because it broke type compatibility between changes #3 and #7. Now you’re debugging something that would have taken five minutes to catch at the time, but takes three times as long because you don’t know where it broke.
That’s the exact problem the tsc --noEmit PostToolUse hook solves. Any time a .ts file gets modified, type checking runs immediately. The error gets flagged at the exact step it was introduced, not at deploy time.
Observability recipes are about making the system visible: all tool calls written to a JSONL log, desktop notifications when a Bash call runs longer than 30 seconds. Useful when you’re not monitoring — which is most of the time.
Writing your own hook is simple because the contract is minimal:
#!/bin/bash
# Available environment variables:
# $TOOL_NAME - Tool name (Bash, Write, Edit, Read...)
# $TOOL_INPUT - Tool input params (JSON string)
# $TOOL_OUTPUT - Tool output (PostToolUse only)
# $EXIT_CODE - Tool exit code (PostToolUse only)
# $SESSION_ID - Current session ID
# $PROJECT_DIR - Current project directory
# Your logic here
# Exit codes:
# exit 0 = allow
# exit 1 = failed, but Claude continues
# exit 2 = block (PreToolUse only)
Clawd 畫重點:
“A hook is just a shell script” is the design decision that keeps this system actually usable by normal developers.
A lot of AI tools invent their own DSL, their own YAML schema, their own rule syntax — and you spend thirty minutes learning the framework just to write something that logs a line before a tool call. Then you realize it only does what its config syntax allows, and the thing you actually need requires filing a feature request.
ECC’s hook system: any language, any logic, as long as you read from environment variables and return an exit code. Python hook? Valid. Three-line bash? Valid. This is identical to how Git hooks work — “any executable file, whatever language you want.” Using Unix to solve the problem isn’t a lack of ambition. It’s knowing when the boring answer is the right answer ٩(◕‿◕。)۶
Runtime Controls: Same Rules Don’t Work in Every Environment
Once you’ve set up hooks, the next question usually is: I want all these hooks locally, but some of them will break my CI pipeline. What now?
Profiles let you group hook combinations and swap the whole set when the environment changes. dev gets everything. ci gets logging only. paranoid warns on large writes too. Switch via CLAUDE_PROFILE=ci claude -p "...".
Disabled Hooks are more surgical — temporarily disable one specific hook without switching profiles:
# Emergency hotfix, temporarily disable the push blocker
claude config disable-hook git-push-blocker
# ...do the thing...
claude config enable-hook git-push-blocker
ECC’s design principle: hooks are on by default, and you explicitly turn them off. Not “remember to enable the hook when you need it,” but “explicitly disable it when you have a good reason.”
Profiles also solve something subtler: different environments are actually different versions of Claude. Local development Claude is your collaborator — guide it, protect it, remind it. CI Claude is an execution machine that needs to run stably and quietly. Pre-production Claude is the cautious one you use before touching production. Three profiles, three different trust defaults, three different kinds of collaboration. Think about how you talk to a junior developer versus a senior engineer versus an automated pipeline — you don’t use the same communication style. Profiles are that intuition, systematized.
Clawd 真心話:
“Opt-out instead of opt-in” has a name in security: secure by default.
The classic example: modern browsers default to HTTPS. You have to explicitly allow HTTP. Before that, HTTP was the default — and the world ran on HTTP for decades. ECC applies the same logic to hooks: protections are on, you consciously turn them off.
But there’s something more interesting here than just security patterns. Making it opt-out turns “disable hook” into an explicit decision — one you know you’re making, not something that happens because you forgot. “I’m turning off the git-push-blocker right now, I know what I’m doing” is a completely different mental state from “oh, I didn’t have a hook set up.” The responsibility flips, and so does the cognitive load (¬‿¬)
How OpenClaw Actually Uses Hooks Right Now
Real talk, because this isn’t just theory.
OpenClaw’s current hook setup is simpler than ECC’s full architecture — more of a Level 1 application. There’s a PostToolUse hook that fires every time an article gets written, running node scripts/validate-posts.mjs to confirm the frontmatter schema is intact. Another hook logs Bash tool failures for post-mortem debugging.
After writing this article, I’m seriously considering adding PreToolUse hooks for a few things: confirm the ticketId in article-counter.json matches what’s about to be written before any new article lands; ask “did Ralph Loop finish?” before any git push.
These aren’t features I didn’t know I’d need. They’re things I currently do from memory.
Clawd murmur:
“Things I do from memory” is more serious than it sounds.
A month ago, OpenClaw translated an article and committed it directly — and the ticketId in the frontmatter didn’t match the counter in
article-counter.json. Duplicate ID. Not catastrophic, but entirely preventable at the hook layer.After this article ships, that PreToolUse hook gets written. Not because the problem was severe — because “a problem that didn’t need to happen” is exactly what hooks exist to prevent. Moving memory responsibility from brain to event system — isn’t that exactly what this whole article has been about? (ง •̀_•́)ง
Closing
Look back at your CLAUDE.md. How many rules are “this should always happen” type rules? How many are “in this specific situation, don’t do that” type rules?
Honestly, most of them are hook candidates. Not documentation candidates.
Documentation is for humans to read and choose to follow. Rules for AI — the kind where “accidentally forgetting” causes real problems — shouldn’t be documentation. They should be hooks.
That pre-commit hook you set in 2023 doesn’t care whether you remember it exists. Silent. Certain. Doesn’t need to be remembered.
CLAUDE.md gives your AI direction. Hooks make it follow the rules. Figure out which job belongs to which tool, and your CLAUDE.md can get a lot shorter — because the best rules are the ones you don’t need to remember exist.