I read every Claude Code hook so you don't have to

hoangnnguyen1 pts1 comments

I read every Claude Code hook so you don’t have to – Codeaholicguy

Skip to content

codeaholicguy

AI, Software Engineering

06/17/202606/16/2026

4 Minutes

When building AI DevKit, I had to look much deeper into different coding agent harnesses. Each one has its own structure, workflow, and mental model.

In this post, I’ll share what I learned from digging into Claude Code hooks.

Claude Code has 27 hook events. I went through all of them.

Most are not worth your time, a handful are useful, one or two can change how you use the tool.

The important thing to note is that hooks let you stop relying on the model to remember your rules. Instead, you can put those rules around the agent.

How hooks work

You put something like this in .claude/settings.json:

"hooks": {<br>"PreToolUse": [<br>"matcher": "Bash",<br>"hooks": [<br>{ "type": "command", "command": "/path/to/hook.sh", "timeout": 30 }

When the event fires, Claude Code runs your command, pipes JSON to stdin, and reads JSON back on stdout.

The input includes the event name, session id, transcript path, cwd, and event-specific fields.

Your stdout decides what happens next. You can abort with continue: false, deny the tool with decision: "block", or inject text into the next model turn with hookSpecificOutput.additionalContext.

Exit 0 with empty stdout is a no-op. Non-zero exits show up in the transcript as a hook error.

That’s it.

What I actually use

PreToolUse

Fires before every tool call. It can approve, block, or rewrite the input via updatedInput.

This is the one that matters most, because rewriting input is different from just observing. You can strip secrets out of commands before they run. You can refuse to run terraform apply outside an allowed directory.

A few examples:

Regex scan for credentials or .env paths in Bash commands, then block with a reason

Auto-redact tokens by rewriting the command instead of blocking it

Hard-deny DELETE or DROP TABLE

If the current branch is main and the command is git push, block

UserPromptSubmit

Fires when you hit enter. It returns additionalContext, which gets injected as a system message for the next turn.

This is basically a dynamic version of CLAUDE.md. Per-turn, conditional on whatever your script can compute.

Some ideas that work:

Compliance reminders when the prompt mentions "user data" or "PII"

A quick grep over the prompt that injects "relevant files: A, B, C"

Refuse to submit if today’s API spend has crossed a threshold

PostToolUse

Fires after a tool call succeeds. It can return additionalContext for the next turn, or updatedMCPToolOutput to rewrite what the model sees.

This is a good place to enforce "always run test after an Edit".

Small thing, but it removes a recurring class of stale-context bugs.

It is also useful for redacting secrets from Bash output before the model sees them. The model does not need to know your access tokens to keep working.

SessionStart

Fires on startup, resume, clear, and after compact. It can return additionalContext, initialUserMessage, or watchPaths.

The source field tells you what kind of start it is, so you can do different things on resume vs fresh boot.

watchPaths is the underrated part. It lets you register paths outside cwd so FileChanged fires for them. That is handy when a shared config repo lives somewhere else.

PermissionRequest

Fires when a tool call is about to ask you for permission. It can allow, deny, rewrite the input, or update persistent permissions.

This gets me off the "yolo allow everything" reflex without going full bypassPermissions.

Auto-allow ls, cat, grep, git status, etc. Defer everything else to the UI. In work repos, deny anything that writes outside the repo. In scratch repos, allow more.

The value is encoding the policy in code instead of clicking through the same prompts forever.

Useful in specific situations

These are not always wired up for me, but I reach for them when the situation calls.

HookBest forPostToolUseFailureAuto-retry transient failures; inject diagnostics like "Bash failed because port 3000 is taken, here’s the PID"PermissionDeniedTell the model why denial happened via additionalContext, or set retry: true with a hintStop"Are you really done?" loops. Inject a checklist and let the model decide if it should keep goingPreCompact / PostCompactPin facts across compaction. Probably the biggest quality win for long sessionsSubagentStopAggregate subagent results into the parent context with structureInstructionsLoadedDebug "where did this rule come from?" The load_reason field tells you whether it came from session-start, nested-traversal, glob-match, include, or compact-restoreNotificationRoute Claude’s idle or awaiting-input alerts through notification, Slack or different channels

PreCompact and PostCompact deserve a callout.

Long sessions get worse over time because compaction throws away things you wanted to keep. Pinning the current task and recent decisions across compaction is a small change,...

claude model fires code hook from

Related Articles