Explainer
What Claude Code hooks are, and why they matter
Most people use Claude Code as a black box: you give it a task, it works, you read the output. But the agent is broadcasting events the whole time it runs — it's about to run a command, it's waiting on you, it just finished its turn. Hooks are how you tap that stream. They're the difference between staring at a terminal hoping to catch the moment it needs you, and having the agent tell you.
The model is refreshingly dumb in the best way: a hook is a shell command that gets a JSON payload on stdin when a lifecycle event fires. No SDK, no plugin API, no daemon to register with. If you can write a shell one-liner that reads stdin, you can hook Claude Code. That's the whole contract, and once it clicks, a lot of "how do I make the agent do X when Y happens" questions answer themselves.
How a hook actually works
You register hooks in your Claude Code settings (settings.json), keyed by event name. When
that event fires, Claude Code spawns your command and pipes a JSON object into its stdin describing what
just happened — which tool, which session, the working directory, and so on. Your command does whatever
it wants with that, and for some events its exit code and stdout feed a decision back into the
agent.
A minimal handler is genuinely a one-liner. Read stdin, pull a field, do something:
#!/usr/bin/env bash
payload="$(cat)"
tool="$(echo "$payload" | jq -r '.tool_name')"
echo "Claude wants to run: $tool" That's it — no special runtime. The important nuance is that hooks aren't all read-only. Some are pure side-effect (fire a notification, write a log); others are decision points where what your script returns changes what the agent does next. That second category is what makes hooks more than a logging mechanism.
One caution up front: exact event names, payload fields, and decision semantics shift between releases. Treat anything below as the shape, not the spec. Before you wire something up, read Anthropic's official hooks documentation for the current names and JSON schema — the structure has changed more than once.
The three events that matter most
Claude Code fires hooks across its lifecycle, but three carry almost all the value for staying out of the terminal. Learn these and the rest are variations.
- PreToolUse — fires before the agent runs a tool (a shell command, a file edit, a fetch). This is the decision hook: your script can inspect the proposed action and return an allow or deny verdict, so the tool runs, gets blocked, or gets routed to you for approval. This is the machinery behind approval gating — "Allow this command?" is a PreToolUse decision surfaced to the user.
- Notification — fires when the agent is waiting on you: a permission prompt it can't proceed past, or an idle nudge when it's been sitting on input for a while. This is your "it needs a decision" signal, and it's the single most useful event for not babysitting, because it fires exactly at the moment your attention has actual value.
- Stop — fires when the agent finishes its turn and hands control back to you. This is how you know a long task is done without watching the scrollback. Pair it with a desktop notifier and you get "Claude finished in api-server" the moment it wraps.
There are more — events around subagents finishing, sessions ending, prompts being submitted — but if you only ever wire up these three, you've covered "is it asking me something," "is it done," and "should this even be allowed to run." Those are the three questions that otherwise force you to keep eyes on the terminal.
What each event is good for
The split is roughly: PreToolUse is for control, Notification and Stop are for awareness. Knowing which bucket you're in tells you whether your hook needs to return a decision or just fire and forget.
- Guardrails with PreToolUse. Auto-deny anything that touches
~/.ssh, blockrm -rfagainst paths outside the repo, or force a manual approve ongit push. Because the hook sees the command before it runs and can veto it, this is real enforcement, not a warning. - Attention routing with Notification. When the agent stalls on a permission prompt, pop a banner, play a chime, or push to your phone if you've stepped away. The point is to fire only on a real waiting state, not on every line of output.
- Completion signals with Stop. Kick off the next step in a pipeline, log how long the turn took, or just tell you it's safe to come back. Combined with running it in the background, this is what lets you launch a task and walk away.
How hooks power gating and notifications
Put the two halves together and you have the whole "stop babysitting" pattern, built from primitives the agent already ships. Approval gating is PreToolUse returning a verdict — allow the safe stuff automatically, deny the dangerous stuff outright, and escalate the gray-area stuff to a human. Awareness is Notification and Stop shelling out to whatever can get your attention.
On macOS that's often terminal-notifier;
on Linux, notify-send. A bare-bones Stop hook is just:
terminal-notifier -title "Claude Code" -message "Agent finished" -sound default This is the right architecture, and it's underused. The catch is what it costs to maintain: you're writing and re-deploying shell glue per machine, parsing payloads by hand to say anything useful, and a bare toast still drops you back to square one — you see "something happened," then you still have to find the right terminal, focus it, and read the scrollback to learn what it wants. The hooks give you the signal; turning that signal into something you can act on without a context switch is the part that takes work. If you want the full ladder of notification options ranked honestly, we covered that in how to actually know when Claude Code needs you.
Where Backgrind fits
Backgrind consumes these same PreToolUse / Notification /
Stop hooks for you, so you don't assemble the shell glue yourself. It wraps your real Claude
Code CLI in an always-on-top overlay: when the
agent needs a decision, asks a question, or finishes, the window flashes and the right tab gets an accent
ring — and because the terminal is already floating over your editor, answering is just typing, no
hunting for a buried window. See it in action in the demo, and if you haven't yet,
install Claude Code first so the hooks have something to fire on.