Claude Code Hooks · Part 5 of 5

The Hooks You Stop Noticing

Abstract series cover: several small softly glowing orbs in a loose constellation on deep navy, linked by faint gold threads, with one larger calm glow among them and soft sound-wave ripples spreading outward - parallel sessions quietly aware of each other
TL;DR
  • The first four posts in this series were about hooks that stop things: block a command, hold an exit, flag a vague prompt. This last one is about hooks that do none of that. They run quietly and change how it feels to work with the agent.
  • TTS narration. A hook on Stop speaks the result of a finished turn, and one on Notification speaks up when a session needs me. I hear state changes instead of watching the terminal for them.
  • A status file. Each session writes its state on SessionStart and refreshes it every time it ends a turn. Glance at one small file and see what every parallel session is doing.
  • Sessions leaving notes. A shared queue file, checked at session start and between turns, lets parallel sessions hand each other messages without me playing courier.
  • Showing up equipped. SessionStart can pre-load a tool, re-scan skills with reloadSkills, and inject this project's conventions through additionalContext, so the agent starts the session already knowing the local rules.

The hooks in the first four parts of this series all announce themselves. A blocked command throws an error in your face. A held exit makes the agent keep working when it wanted to stop. A flagged prompt staples a visible note onto your message. You feel each one the moment it fires. The hooks in this post are the opposite. The best one I run is a hook I forget exists, because it never interrupts anything. It just sits in the background and quietly changes the texture of working with the agent.

This is part 5, the last one. Part 1 was context rot and the handoff that resets a session before it decays. Part 2 was guardrails that catch a dangerous command before it runs. Part 3 was quality gates that won't let the agent say "done" without proof. Part 4 was the prompt layer, a local model reading my input before Claude does. Those were all about control. This one is about comfort, and it lives on three events that never block a thing: SessionStart, Notification, and Stop.

Hooks that whisper instead of shout

Every hook in the earlier parts had a verdict. Allow this, block that, here's a note you should read. The ambient ones have no verdict at all. They observe an event and produce a side effect somewhere off to the side: a sound, a line in a file, a message dropped in a queue. The agent's turn is completely unaffected. Nothing is held, nothing is denied, nothing is injected into the conversation unless I choose to.

That sounds like a smaller thing than a guardrail, and on any single turn it is. The payoff shows up over a week. After living with these for a while, the hooks stopped feeling like rules I imposed on the agent and started feeling like the agent's interface to me. They are how I keep a sense of what's going on without staring at a wall of scrolling text. That shift, from policing the agent to quietly living alongside it, is the whole reason I wanted to end the series here.

Hearing the agent instead of watching it

The first ambient hook I ever wrote reads the agent's turns out loud. There was nothing clever behind it. I was tired of babysitting a terminal. I'd kick off a long task, then sit there watching lines scroll, afraid to look away in case it finished or got stuck. Text-to-speech let me look away.

Here's the part the marketing version of this gets wrong, and I want to be precise about it. There is no hook in Claude Code that fires on every little thing the agent does mid-turn. The events I have to work with are turn boundaries and notifications. So the narration isn't a live play-by-play of every file edit. It's narration of state changes, which turns out to be exactly what I actually want.

Two events carry it. Stop fires when the agent finishes a turn, so a hook there can speak a short summary of what just happened: "finished, tests green" or "stopped to ask you something." And Notification fires when Claude Code wants my attention, which is the real moment I need to hear about. The input is small:

{
  "hook_event_name": "Notification",
  "message": "Claude needs your permission to run a command",
  "session_id": "abc123",
  "cwd": "/path/to/project"
}

One thing to know before you build on this: the Notification hook is informational only. It cannot block, it cannot change anything, its exit code is ignored. That's perfect for narration, because narration should never be able to interfere with the work. The hook reads the message field, pipes it to a text-to-speech voice, and gets out of the way. It also fires on a fixed set of moments rather than continuously, things like the agent going idle or asking for permission, so what I hear is "your turn" and "I'm done," never a firehose. I can go make coffee, hear the voice say the build passed, and wander back. The terminal stopped being something I have to guard.

The trick with audio is to narrate state changes rather than activity. You don't want to hear every tool call. You want to hear the two moments that matter: it needs you, or it's done. Notification and Stop map onto exactly those two, which is why this works as well as it does.

A small file that says what each session is doing

The minute you run more than one Claude session at a time, you lose the plot. Three terminal tabs, each grinding on something, and no way to tell at a glance which one is waiting on you and which one is happily working. I fixed that with a hook and a single tiny file.

On SessionStart, each session writes a line into a shared status file: who I am, what I'm working on, started just now. The event hands the hook everything it needs to identify itself:

{
  "hook_event_name": "SessionStart",
  "source": "startup",
  "session_id": "abc123",
  "cwd": "/path/to/project"
}

The source field tells the hook how the session began. It's one of startup, resume, clear, or compact, so a hook can write "fresh start" differently from "resumed an old session" if it cares to. Then, on every Stop, the same session updates its line: still working, or now idle and waiting. The status file becomes a live board. One small file each session keeps current, and a glance at it tells me the state of everything running. No dashboard server, no daemon, just a file that every session politely keeps honest as it goes.

The reason this is a hook and not a feature I bolted onto my workflow is that I will absolutely forget to update a status by hand. The hook never forgets. It fires on SessionStart and Stop whether I'm paying attention or not, so the board is never stale. That's the recurring theme of this whole series: the things you mean to do every time are the things a hook should do for you.

Sessions leaving notes for each other

Once each session is writing where I can see it, the next thought is obvious: can they talk to each other? Not in real time, they're separate processes that don't share memory. But they can leave notes. A shared queue file, one session appends a message addressed to another, and the recipient picks it up the next time one of its hooks runs.

The honest mechanical detail here matters, because it's easy to imagine this as live chat and it isn't. Hooks are event-triggered rather than background daemons. A session can only check its inbox at the moments its hooks fire: at SessionStart, it reads any messages left while it was away, and on Stop, between turns, it peeks at the queue again. So the delivery is "the next time you come up for air" rather than "instantly." In practice that's plenty. One session finishes a piece of research and leaves a note for another that's been waiting on it; the waiting session sees it the next time it ends a turn and picks up the thread. I stopped being the courier carrying findings from one tab to another.

Two parallel Claude sessions and an ambient layer between them. On SessionStart each session writes its state to a shared status file and reads any queued messages. On Stop, at each turn boundary, a session refreshes its status, leaves a note in the queue for another session, and speaks the result out loud through text to speech. You glance at the status file and hear the narration without watching the terminal. Session A SessionStart / Stop Session B SessionStart / Stop Status file who, what, idle? Message queue notes between sessions You glance one quick look You hear it TTS on Stop read state speak result
The ambient layer: sessions write state and leave notes at their SessionStart and Stop boundaries. You take it in by glancing at one file and listening, instead of watching every terminal.

Showing up already equipped

The other thing SessionStart is good for is making the agent useful from its first message instead of its fifth. A fresh session is a blank slate. It doesn't know which project it's in, what the local rules are, or which tools this particular job needs. Normally I'd spend the first few exchanges telling it. A hook can hand it all of that before I type anything.

There are three concrete moves here, and unlike the narration story, these come straight from documented fields:

The shape of the output is the familiar one. Print JSON, and the context rides into the session:

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Project conventions: branch off main, never commit to it directly. Reports go in the project report folder, not the repo root."
  }
}

Note the casing, because it's a silent-failure trap. The input field is hook_event_name in snake_case; the output key is hookEventName in camelCase. Get one wrong and the hook runs, exits clean, and does precisely nothing, which is the most annoying kind of bug to chase. If you'd rather skip the JSON entirely, a SessionStart hook can just print plain text to stdout on a clean exit and Claude Code folds that into the context too. The JSON form is for when you also want to flip reloadSkills or set a session title.

Where this stops working

Ambient hooks fail quietly, which is both their charm and their risk. A guardrail that breaks throws an error you can't miss. A status writer that breaks just stops updating, and you might not notice for an hour that the board has gone stale and you've been trusting a frozen snapshot. So the failure mode flips compared to the earlier posts: there, a broken hook was loud and safe; here, a broken hook is silent and quietly misleading. I keep these dead simple for exactly that reason. The more an ambient hook does, the more ways it has to lie to you without saying a word.

The other limit is restraint. It is genuinely tempting to narrate everything, write five status files, wire up elaborate cross-session protocols. Don't. The value of the ambient layer is that it's quiet. The moment it starts demanding attention, narrating noise, filling files you have to parse, it's just another thing shouting at you, and you've lost the one property that made it worth building. The good version fades into the background. The bad version becomes a second job.

Build your own

The pattern is the smallest in the whole series, because these hooks don't decide anything:

  1. Pick an event with no verdict: SessionStart for setup, Stop for turn boundaries, Notification for "needs you" moments.
  2. Read the JSON on stdin. Pull source on SessionStart, message on Notification, session_id when you need to tell sessions apart.
  3. Produce a side effect off to the side: speak a line, append to a status file, drop a note in a shared queue. Never touch the agent's turn.
  4. To feed the session at startup, return hookSpecificOutput.additionalContext (camelCase) or just print plain text. Add reloadSkills if you need skills re-scanned.
  5. Keep it boring and keep it quiet. An ambient hook earns its place by being something you stop noticing, not something you have to manage.
Series: Claude Code hooks (5 parts)
  1. Context rot + handoff - how I stop sessions from rotting
  2. Guardrails - hooks that won't let the AI shoot me in the foot
  3. Quality gates - forcing the agent to verify before it says "done"
  4. The prompt layer - a local LLM that reads my prompt before it hits Claude
  5. The ambient layer - the hooks you stop noticing (you're here)

That's the series. Five posts, from the loud hooks to the quiet ones.

The whole series, in one line each

Five posts, and a shape underneath them. It started with context rot: a session that runs too long quietly gets worse, so a hook resets it before it decays. Then guardrails, catching the one dangerous command before it runs. Then quality gates, refusing to let "done" mean anything less than proof. Then the prompt layer, a local model reading my half-formed input before the expensive one ever sees it. And finally this, the ambient layer, where the hooks stop being walls I built around the agent and become the way I keep a calm sense of what it's doing. Each one is a small piece of judgment I got tired of supplying by hand, handed to the deterministic part of the system that never gets tired and never forgets. That, more than any single hook, is what the whole series was about.

Takeaway

The loud hooks get the credit. They block the disaster, hold the exit, catch the bad prompt, and you feel grateful the moment they fire. The quiet ones get none of that, and they might be the ones I'd miss most if they vanished. A voice that tells me the build passed while I'm across the room. A file that shows me what four sessions are doing in one glance. Sessions that hand each other notes so I don't have to. None of it blocks anything. All of it changed how it feels to work with the agent, which is the part the loud hooks never touch.