← All posts

Claude Code Notifications: Never Miss When It Needs You

Claude Code goes quiet when it needs input. Here are real Notification and Stop hook configs for terminal bells, macOS alerts, and per-tab badges.

Claude Code has one genuinely annoying failure mode. It does not fail. It waits.

You give it a task, you switch to your browser to read something for two minutes, and when you come back the agent has been sitting on a permission prompt for ninety seconds. Nothing crashed. No error. It just needed a yes and had no way to tell you.

I timed this on myself for a week. Roughly eleven minutes a day of pure dead agent time, on one session. With three sessions it was worse, because I was also spending time cycling through tabs looking for the stuck one.

The fix is not discipline. The fix is hooks. Here is how to wire them up.

The babysitting problem

The reason this happens is that a CLI agent has no way to interrupt you. It writes to stdout and waits on stdin. If that terminal is not in front of your face, the request is invisible.

There are exactly two moments you care about:

  1. The agent is blocked on you. A permission prompt, or it has been idle long enough that it is clearly expecting input.
  2. The agent finished. The turn is done, there is a diff to read.

Everything else is just work happening, and you do not want to be told about it. A notification scheme that fires on every tool call is worse than nothing, because you learn to ignore it in about a day.

Claude Code exposes both of those moments as hooks. Notification fires for the first. Stop fires for the second. That is the whole API surface you need.

What hooks actually are

A Claude Code hook is a shell command Claude Code runs when a lifecycle event happens. You declare them in settings.json, either at ~/.claude/settings.json for everything you do or .claude/settings.json inside a project.

Claude Code pipes a JSON blob to your command's stdin describing the event. Your command can ignore it entirely, which most notification hooks do, or parse it if you want detail like the session ID or the working directory.

The events relevant here:

Event Fires when
Notification Claude needs permission, or has gone idle waiting on input
Stop Claude finishes a turn
SubagentStop A spawned subagent finishes
PreToolUse / PostToolUse Around every tool call. Useful for clearing state, too noisy for alerts
The five-second version: terminal bell

If you want something working before you finish this paragraph, use the bell. Put this in ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          { "type": "command", "command": "printf '\\a'" }
        ]
      }
    ]
  }
}

That is it. Your terminal emulator turns the bell character into whatever you have configured: an audible beep, a visual flash, a dock badge, a tab marker. Most terminals will badge the tab, which is genuinely useful when the window is behind something else.

Two catches. Some terminals ship with the bell disabled by default, so check your settings if nothing happens. And a bell tells you something wants you, not which session, which starts to matter the moment you run more than one.

The one I actually use: macOS notification

On a Mac, osascript gives you a real notification with text in it. This is the config I run:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Waiting on you\" with title \"Claude Code\" sound name \"Submarine\"'"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Turn finished\" with title \"Claude Code\" sound name \"Glass\"'"
          }
        ]
      }
    ]
  }
}

Two different sounds is the trick. After a couple of days you stop reading the notification at all. Submarine means go look, Glass means there is a diff waiting. Your ears do the triage.

A few notes from using this daily:

  • macOS will ask permission the first time. If notifications never appear, check System Settings, Notifications, Script Editor.
  • Drop sound name if you work near other people. The visual banner still lands.
  • Keep the command fast. Hooks run synchronously, and a slow hook makes the agent feel sluggish.
Including which project it came from

If you want the notification to say which repo it is, parse the JSON on stdin. Save this as ~/.claude/notify.sh and chmod +x it:

#!/usr/bin/env bash
dir=$(basename "$(jq -r '.cwd // "."' <<< "$(cat)")")
osascript -e "display notification \"Waiting in ${dir}\" with title \"Claude Code\" sound name \"Submarine\""

Then point the hook at ~/.claude/notify.sh. Requires jq. Worth it once you have more than two projects going.

Linux and Windows

Same idea, different command. On Linux use notify-send "Claude Code" "Waiting on you". On Windows, a PowerShell toast or, honestly, just the bell.

Why the status line does not solve this

People reach for the status line first, and it is the wrong tool for this specific job.

The status line is great. It shows model, context budget, git branch, token spend, and I keep one configured. We wrote up the details in customizing your Claude Code status line.

But it has a fundamental limitation for notifications: it only exists inside the session you are looking at. If the terminal is behind your browser, the status line is behind your browser too. It cannot tell you about a session you are not currently watching, which is exactly the case you need help with.

The status line answers "what is the state of this session." Notifications answer "which session needs me." Different questions.

How MOLTamp binds hooks to tabs

Full disclosure, this is our app, so weigh accordingly.

The gap in everything above is attribution. A bell says something happened. An OS notification says something happened and maybe in which directory. Neither one tells you which tab, and with six sessions open that is the only fact you actually need.

MOLTamp closes that loop by registering its own hook entries in ~/.claude/settings.json, tagged so it can find and update them later, then mapping each event back to the tab that produced it. Since v3.2.0 that drives per-tab attention badges:

  • Amber on Notification. That session is blocked on you.
  • Green on Stop. That session finished while you were elsewhere.
  • Badge clears on the next PreToolUse or PostToolUse, because tool activity means the agent is working again and whatever it wanted is resolved.

Badges only fire for tabs that are not focused. The one you are staring at does not need a badge, and marking it would train you to ignore all of them.

Two design decisions worth calling out. The badge colors come from your terminal palette, --t-yellow and --t-green, so a skin inherits sensible colors for free instead of hardcoding a clashing amber. And the pulse animation is applied as an inline style specifically so no skin can override it. A theme should be able to restyle the entire tab bar, but it should never be able to make "this session needs you" invisible. The reasoning is written up in the skinning docs, and the community gallery has tab bar treatments worth borrowing.

MOLTamp's hook entries coexist with yours. It only touches its own tagged entries, so the osascript config above keeps working alongside it. I run both: badges for which tab, OS notification for when the whole window is hidden.

Tuning it so you do not go numb

The failure mode of notifications is habituation. A few things that kept mine useful:

Never hook PreToolUse or PostToolUse for alerts. They fire constantly. You will mute everything within an hour.

Use different sounds for waiting and done. They demand different urgency. Waiting is blocking. Done can wait for you to finish your sentence.

Hook SubagentStop only if you run long parallel agent fan-outs. Otherwise it is noise. See subagents explained simply for when that pattern applies.

Keep hook commands under about 100ms. They are synchronous. A hook that shells out to a network call will make the agent feel broken.

Test with a throwaway prompt. Ask the agent to do something that triggers a permission prompt and confirm both hooks fire. Silent misconfiguration is the norm here, because a hook that fails just prints nothing.

FAQ
How do I get notified when Claude Code needs input?

Add a Notification hook in settings.json that runs a terminal bell (printf '\a') or an OS notification (osascript on macOS, notify-send on Linux). Notification is the event that fires on permission prompts and idle waits.

What is the difference between the Notification and Stop hooks?

Notification means Claude is blocked and needs something from you. Stop means Claude finished its turn. Give them different sounds so you can tell them apart without looking.

Why is my Claude Code hook not firing?

Usually one of three things: invalid JSON in settings.json, a command that is not on PATH when Claude Code runs it, or a script without the execute bit. Run the command by hand first. Also restart your session after editing settings.

Can I see which session needs me when running several at once?

Not from the bell or a plain OS notification, since neither carries tab identity. Either parse cwd from the hook JSON and include it in the notification text, or use a shell like MOLTamp that maps hook events back to individual tabs.


Start with the bell, upgrade to osascript once you want to know what happened without switching windows. If you want the deeper tour of every hook event and what the JSON payload contains, read the Claude Code hooks guide. And if you are running enough sessions that "which one" has become the real question, MOLTamp is free and does that part for you.