DEV Community

Eren Çolak
Eren Çolak

Posted on Fully Autonomous

How to get notified when Claude Code finishes or needs permission

If you run Claude Code in the VS Code or Cursor terminal, you get no alert when it finishes or stops for a permission prompt. Claude Code sends a desktop notification on its own only in a few terminals (Ghostty, Kitty, iTerm2). In the VS Code terminal, Warp and most others it sends nothing by default.

So you keep checking the tab, or you switch away and come back twenty minutes later to a session that has been waiting for a "yes" the whole time.

Here are three fixes, from a one-line setting to a hook that tells you which session it is. Checked against Claude Code 2.1.x on macOS, Linux and Windows.

1. One line: the terminal bell

Tell Claude Code to ring the terminal bell whatever terminal you use. In ~/.claude/settings.json:

{
  "preferredNotifChannel": "terminal_bell"
}
Enter fullscreen mode Exit fullscreen mode

In VS Code and Cursor, the bell is silent until you turn on its sound. Open Settings, search for Terminal Bell (under Accessibility Signals) and set the sound to on.

Good: zero scripts. Not so good: it's a short beep, you have to be in earshot, and it doesn't say which session rang.

2. A hook that pops a desktop notification

Hooks are commands Claude Code runs at points in its lifecycle. Two events matter here:

Event When it fires
Stop Right when Claude finishes its turn
Notification with permission_prompt Claude needs you to approve something and the prompt has waited about six seconds
Notification with idle_prompt About 60 seconds after a finished turn, if you haven't typed since

Stop is the one you want for "it's done". idle_prompt repeats the same news a minute later, so if you hook both you get two alerts for one finished turn.

Add this to ~/.claude/settings.json (macOS shown; other systems below):

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "osascript -e 'display notification \"Your turn\" with title \"Claude Code\"'",
        "async": true
      }]
    }],
    "Notification": [{
      "matcher": "permission_prompt",
      "hooks": [{
        "type": "command",
        "command": "osascript -e 'display notification \"Needs permission\" with title \"Claude Code\"'",
        "async": true
      }]
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

"async": true matters more than it looks. Hooks are synchronous by default: Claude waits for each one. A notification command that takes a second or three (PowerShell's startup alone is about that on Windows) slows every turn down. With async, it runs in the background.

The command for other systems:

Linux (needs a notification daemon, so not over plain SSH):

notify-send 'Claude Code' 'Your turn'
Enter fullscreen mode Exit fullscreen mode

Windows (plays the system chime; a message box would block until you click it):

powershell.exe -NoProfile -Command "(New-Object Media.SoundPlayer 'C:/Windows/Media/chimes.wav').PlaySync()"
Enter fullscreen mode Exit fullscreen mode

The forward slashes are on purpose: they work in Windows paths and need no escaping inside JSON. Inside settings.json, escape the double quotes as \".

Sound only, macOS:

afplay /System/Library/Sounds/Glass.aiff
Enter fullscreen mode Exit fullscreen mode

If nothing shows on macOS, osascript notifications go through Script Editor. Run osascript -e 'display notification "test"' once in Terminal so Script Editor appears in System Settings > Notifications, then allow it.

3. Say which session it is

With one session, "Your turn" is enough. With three or four, you need to know which one. Every hook gets a JSON object on stdin, and it includes cwd, the folder the session runs in. Put that in the notification (macOS, needs jq):

{
  "type": "command",
  "command": "osascript -e \"display notification \\\"$(jq -r '.cwd' | xargs basename)\\\" with title \\\"Claude Code: your turn\\\"\"",
  "async": true
}
Enter fullscreen mode Exit fullscreen mode

Now the notification says api or web instead of just "Claude Code". On Linux, the same idea is notify-send "Claude Code: your turn" "$(jq -r '.cwd' | xargs basename)".

Things that tripped me up

  • Hooks add up, they don't replace. Your user, project and local settings all contribute hooks, and they all run, in parallel. If a project's .claude/settings.json already has a Stop hook, yours runs next to it.
  • The permission alert is delayed on purpose. permission_prompt waits about six seconds, so a prompt you answer right away never pings you. Don't "fix" it by hooking PermissionRequest instead unless you want an alert for every single approval.
  • Being in the window doesn't mean you're looking. The hook can't tell whether you're watching that terminal. If the alerts get annoying while you're at your desk, keep only the permission one and drop Stop.
  • Some terminals show status by themselves. Terminals that support the OSC 7501 status protocol show whether each session is working or waiting, with no setup, from Claude Code 2.1.295 on. Check your terminal's docs.

When there are more than a few sessions

Past three or four sessions, notifications help but you still have to find the right tab. That's the problem I built Paneherd for: a VS Code / Cursor extension that opens each session as a terminal in a grid and labels every pane Working, Needs permission or Your turn, using the same hooks as above. Notifications and sounds are in the free version. There are other extensions for this in the Marketplace too, and for a single session the setting in step 1 is all you need.

What do you use: the bell, a hook, or something else?

Top comments (0)