Guide
Get notified when Claude Code finishes or needs input
Claude Code has a built-in alert, but by default it only shows a desktop notification in iTerm2, Ghostty and Kitty. To be told in any terminal on a Mac, add two hooks to ~/.claude/settings.json: a Stop hook, which runs when Claude finishes responding, and a Notification hook matched on permission_prompt, which runs when Claude is waiting for your approval. Each hook can run osascript for a banner, afplay for a sound, or both.
If you only want the snippet, the notification hook generator builds it from a few checkboxes.
What Claude Code does before you add any hooks
Claude Code raises a notification when it finishes a task or pauses for a permission prompt and you appear to be away from the terminal. The preferredNotifChannel setting, shown in /config as Local notifications, decides how that reaches you. The default is "auto":
- in iTerm2, Ghostty and Kitty, a desktop notification;
- in Terminal.app, the bell character, and only when you have turned Terminal's audible bell off;
- in any other terminal, nothing.
To get the bell in every terminal, add this to ~/.claude/settings.json:
{
"preferredNotifChannel": "terminal_bell"
}
The other accepted values are "iterm2", "iterm2_with_bell", "kitty", "ghostty" and "notifications_disabled". Two terminals need extra setup before the built-in notification gets through:
- iTerm2: open Settings > Profiles > Terminal, check "Notification Center Alerts", click "Filter Alerts" and enable "Send escape sequence-generated alerts".
- tmux: add
set -g allow-passthrough onto~/.tmux.confand runtmux source-file ~/.tmux.conf. Without it, tmux swallows the notification before the outer terminal sees it.
Hooks go further: you choose the event, the text and the sound, and they work in terminals where "auto" does nothing.
Which hook events mean finished and which mean waiting
A hook is a command Claude Code runs when a named event happens. These are the events that matter here:
Stop, no matcher- Every time Claude finishes responding, whether or not you are at the keyboard. Not when you interrupt, and not when the turn ends in an API error (that fires
StopFailure). Notificationwith the matcherpermission_prompt- Claude needs you to approve a tool use, and the prompt has waited about six seconds. In a terminal session each keystroke restarts the wait.
Notificationwith the matcheridle_prompt- Claude finished responding about 60 seconds ago, you have not typed since, and no background agent is still running.
PermissionRequest, matched on a tool name such asBash- The moment Claude Code is about to ask for permission to use a tool, with no delay.
Stop takes no matcher; one added there is silently ignored. For Notification, the matcher is the notification type. Leave it out to run the hook for every type, which also covers elicitation_dialog (an MCP server asking you for input), auth_success and several others, or join types with a pipe: "permission_prompt|idle_prompt". Matchers are case-sensitive.
Choosing between Stop and idle_prompt is choosing how much noise you want. Stop fires immediately on every turn, including the short ones you are watching. idle_prompt waits a minute and only fires if you have not come back.
The settings.json for a banner and a sound
This block shows a banner and plays a sound when Claude finishes a turn and when a permission prompt has been waiting:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Finished responding\" with title \"Claude Code\"'"
},
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
],
"Notification": [
{
"matcher": "permission_prompt",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Waiting for your permission\" with title \"Claude Code\"'"
},
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
}
}
The shape has three levels. Under hooks, each key is an event name. Its value is a list of matcher groups, each with an optional matcher and its own hooks list. Each entry in that inner list is a handler: "type": "command" and the command to run, which Claude Code passes to sh -c on macOS. Handlers that match run in parallel.
Both commands ship with macOS. osascript -e 'display notification …' shows a banner, which is silent on its own. afplay plays an audio file; ls /System/Library/Sounds lists the system sounds.
Put the block in ~/.claude/settings.json to cover every project on your Mac. A project's .claude/settings.json is the file a team commits, so teammates on other systems would inherit macOS-only commands; for one project, use .claude/settings.local.json. If the file already has a hooks key, put the Stop and Notification entries inside it. A second hooks key does not merge with the first: only the last one is used.
Three things to know about how these run:
- Hooks run alongside the built-in notification, not instead of it, so in iTerm2, Ghostty or Kitty you get both. Set
"preferredNotifChannel": "notifications_disabled"to keep only the hook; hooks still run with that value. - Claude Code waits for a hook to finish, and
afplayruns for the length of the sound, a second or two."async": trueon a handler runs it in the background. Leave it off forclaude -p, where an async hook still running at exit is killed. - To have the banner come from your terminal instead of
osascript, a hook can return aterminalSequencefield, and Claude Code emits the notification escape sequence for you. The hooks reference has the script.
Name the project in the banner
With several sessions open, "Finished responding" does not say which one. Claude Code sets CLAUDE_PROJECT_DIR in the hook's environment to the folder the session started in, so the command can name it:
{
"type": "command",
"command": "osascript -e 'on run argv' -e 'display notification \"Finished in \" & (item 1 of argv) with title \"Claude Code\"' -e 'end run' \"$(basename \"$CLAUDE_PROJECT_DIR\")\""
}
The folder name reaches the script as an argument instead of being pasted into it, so a quote or an apostrophe in the name cannot break the command.
Test each layer separately
- Run the commands by hand. Paste
osascript -e 'display notification "Finished responding" with title "Claude Code"'and thenafplay /System/Library/Sounds/Glass.aiffinto a terminal. No banner here means the problem is in macOS, not Claude Code. - Check that Claude Code loaded the hooks. Type
/hooksin a session and confirm they are listed underStopandNotification. Edits to the file are normally picked up without a restart. - Trigger
Stop. Send a short prompt, or runclaude -p "Reply with the word ok". The banner and sound arrive as the reply ends. - Trigger
permission_prompt. Ask Claude to run a command that needs approval, then keep your hands off the keyboard for more than six seconds. - Read the debug log. Start with
claude --debug-file /tmp/claude.logand runtail -f /tmp/claude.login another terminal to see which hooks matched and how they exited.
Why a hook runs and you still get nothing
- Script Editor is not allowed to notify.
osascriptbanners are delivered as the built-in Script Editor app, not as your terminal. Without notification permission for Script Editor the command fails silently, and macOS does not ask. Runosascript -e 'display notification "test"'once, then open System Settings > Notifications, find Script Editor and turn on Allow Notifications. - A Focus is on. A Focus, including Do Not Disturb, silences notifications from apps you have not allowed. Turn it off, or open System Settings > Focus, select the Focus, click Allowed Apps and allow the app that sends the banner.
- The banner was hidden or brief. System Settings > Notifications has a separate switch for notifications while mirroring or sharing the display, and Script Editor's Persistent style keeps a banner on screen until you dismiss it.
- The sound is muted.
afplayis ordinary audio playback, not a notification. Focus and notification permissions do not apply to it; output volume and mute do. - You were typing.
permission_promptneeds about six seconds without a keystroke andidle_promptabout 60, so testing with your hands on the keyboard looks like a broken hook. - The JSON did not load. Trailing commas and comments are not allowed, and with either one
/hookslists nothing from the file.python3 -m json.tool ~/.claude/settings.jsonprints the file if it parses and the position of the error if it does not. Twohookskeys parse without an error, but only the last is used. - Hooks are switched off.
"disableAllHooks": truein your settings turns off every hook. If/hookssays only hooks from managed settings run here, your organization has blocked user hooks. - The failure is hidden. For
Notificationhooks, Claude Code ignores the exit code and stderr, so a mistyped command shows no error outside the debug log. A failingStophook does show a hook error notice in the transcript. - It is the built-in notification that is missing. Check the terminal list above, and confirm that your terminal app, not Script Editor, has notification permission.
Notifications on your phone
Hooks alert the Mac you are sitting at. For a phone, Claude Code sends push notifications through Remote Control, which needs a Pro, Max, Team or Enterprise plan and does not work with API keys:
- Install the Claude app for iOS or Android, sign in with your Claude Code account and allow notifications.
- In Claude Code, run
/configand enable Push when actions required (permission prompts and questions), Push when Claude decides (such as a long task finishing), or both. - Connect the session with
claude --remote-control, or/remote-controlinside one. Pushes are sent only while Remote Control is connected, and not while you are typing in or focused on that terminal.
The Codex CLI equivalent
Codex has two mechanisms, both set in ~/.codex/config.toml: notifications from its terminal UI, which are on by default, and a notify program that it runs for you.
notify = ["osascript", "-e", "display notification \"Codex finished a turn\" with title \"Codex\""]
[tui]
notifications = ["agent-turn-complete", "approval-requested"]
notification_method = "auto"
notification_condition = "unfocused"
tui.notificationsistrue,false, or a list of event types such as the two above.notification_methodisauto,osc9orbel;autoprefers an OSC 9 desktop notification and falls back to the bell.notification_conditionisunfocused(the default) oralways.notifyruns an external program, written as a list of arguments, when a turn completes.agent-turn-completeis currently its only event, so it cannot tell you about approval requests;tui.notificationscan.- Codex appends one JSON argument describing the turn.
osascriptignores it, butafplayrejects a second argument, so wrap a sound in a shell:notify = ["sh", "-c", "afplay /System/Library/Sounds/Glass.aiff"]. notifymust sit above the first[section]header, or TOML reads it as part of that section. Codex ignoresnotifyin a project's.codex/config.toml, so set it in the user file.
Codex also has a separate hooks system with Stop and PermissionRequest events, described in its hooks documentation.
When several agents are waiting at once
A notification says that something wants you. With agents running in five projects, the harder questions are which one and what it is asking. Naming the project in the banner helps, and you still have to find the right terminal to answer.
Armadai, a native macOS app that runs Claude Code and Codex in separate project workspaces on one canvas, handles this with a list instead of an alert. Its sidebar shows a status beside each session, working, idle or needs approval, across all your projects. When a session is waiting you hover it, read the request and approve it from the sidebar, and your canvas stays where you left it. The choices offered depend on the agent and the request, and a prompt that needs more context or is not supported in the sidebar is answered by opening its session.
Armadai costs $20 a month after a 7-day free trial that requires a payment method, and it has no free plan. The homepage has an interactive illustration of that sidebar.
Questions
Does the Stop hook fire when Claude ends its reply with a question?
Yes. Stop fires whenever Claude finishes responding, not only when a task is complete, so a turn that ends in a question triggers it too. It does not fire when you interrupt Claude, and a turn that ends in an API error fires StopFailure instead.
Why does the permission notification arrive a few seconds late?
That is how the permission_prompt notification type is timed: Claude Code sends it once the prompt has waited about six seconds, and in a terminal session each keystroke restarts the wait. For a signal the moment Claude asks, hook the PermissionRequest event instead.
Do hooks still run if I turn off Claude Code's own notifications?
Yes. The preferredNotifChannel setting, including the notifications_disabled value, changes only how Claude Code itself alerts you. Notification hooks run either way, which is useful when you want the hook to be the only alert.
Can I use a different sound?
Yes. afplay takes the path of an audio file, and the system sounds live in /System/Library/Sounds. Run ls /System/Library/Sounds to see the names on your Mac, then change Glass.aiff in the command to the one you want.
Do these hooks run in Claude Code cloud sessions?
Not from your user settings. Cloud sessions do not read your local ~/.claude/settings.json, and osascript and afplay exist only on macOS, so these hooks are for sessions running on your own Mac.
Sources
Details about other products come from their own pages, checked on the dates shown. If something has changed, tell us at support@armadai.sh and we will correct it.
- Claude Code's Stop hook runs when Claude finishes responding and takes no matcher; Notification hooks match on types such as permission_prompt (after about six seconds) and idle_prompt (after about 60 seconds); PermissionRequest runs the moment Claude Code is about to ask. code.claude.com, checked October 4, 2026.
- Claude Code's hooks guide gives the osascript notification hook for macOS and says the banner is routed through Script Editor, which needs notification permission. code.claude.com, checked October 4, 2026.
- By default Claude Code sends a desktop notification only in Ghostty, Kitty and iTerm2; iTerm2 needs alert forwarding enabled and tmux needs allow-passthrough. code.claude.com, checked October 4, 2026.
- Claude Code's preferredNotifChannel setting accepts auto, terminal_bell, iterm2, iterm2_with_bell, kitty, ghostty and notifications_disabled, and defaults to auto. code.claude.com, checked October 4, 2026.
- Claude Code sends push notifications to the Claude mobile app while Remote Control is connected, switched on in /config; Remote Control needs a Pro, Max, Team or Enterprise plan. code.claude.com, checked October 4, 2026.
- Codex runs the notify program on agent-turn-complete with a JSON argument, ignores notify in project config files, and has tui.notifications, tui.notification_method and tui.notification_condition (developers.openai.com/codex redirects to this site). learn.chatgpt.com, checked October 4, 2026.
- Codex's sample config documents tui.notifications as a boolean or filtered list that defaults to true, with agent-turn-complete and approval-requested as example types. learn.chatgpt.com, checked October 4, 2026.
- Codex has its own hooks, including Stop and PermissionRequest, and requires each hook to be reviewed and trusted before it runs. learn.chatgpt.com, checked October 4, 2026.
- On a Mac, a Focus pauses and silences notifications except from the people and apps you allow. support.apple.com, checked October 4, 2026.
- macOS Notifications settings have a per-app Allow notifications switch, Temporary and Persistent styles, and an option for notifications while mirroring or sharing the display. support.apple.com, checked October 4, 2026.