Guide

Claude Code is waiting for permission: how to know, and how to answer

Claude Code blocks silently. It stops at a command it will not run without you and waits in a terminal tab you are probably not looking at, and by default nothing anywhere else on your Mac says so. Here is exactly which events fire when that happens, why the terminal bell keeps missing you, and the three ways to fix it - in order of how much work each one is.

Applies to Claude Code on macOS · payloads from Anthropic's hooks docs

What actually fires when Claude Code blocks

Three different events are in play when a run stops, and knowing which is which is most of the battle.

  • PreToolUse fires before every tool call, whether or not it needs a decision. It is not a "waiting" signal, so hanging a notification off it means a banner for every file read.
  • PermissionRequest fires when a tool call actually needs a decision. This is the blocking one. It receives tool_name, tool_input, tool_use_id, session_id, cwd and permission_mode, and answers with a hookSpecificOutput.decision of allow, deny or prompt. Exit code 2 is not honoured here; that decision object is the only way to answer.
  • Notification fires when Claude Code wants to tell you something and carries a notification_type. Two values matter here: permission_prompt, when it is waiting on a decision, and idle_prompt, after roughly a minute of the prompt sitting untouched.

So the event you want is Notification, matched on permission_prompt. Its payload:

{ "session_id": "abc123", "transcript_path": "/Users/you/.claude/projects/.../transcript.jsonl", "cwd": "/Users/you/work/api", "permission_mode": "default", "hook_event_name": "Notification", "message": "Claude needs your permission to use Bash", "title": "Claude Code", "notification_type": "permission_prompt" }

Note what is not in there. You get cwd and session_id, enough to say which session is waiting, which is the thing that matters once you run more than one. You do not get the command: message is a sentence, not the tool input. For the command itself you need PermissionRequest, whose tool_input carries it.

Why the terminal bell misses you

Claude Code does not draw macOS notifications itself. It hands the notification to the terminal it is running in, and terminals differ wildly in what they do with it - which is why the same setup looks broken in one terminal and perfect in another with nothing changed between them.

The channel that works everywhere is the bell, and the bell fails for ordinary reasons: it is off in your terminal profile, the terminal is on a Space you cannot see, the Mac is muted, Do Not Disturb is on. It is also stateless. It happens once and leaves nothing behind, so a prompt that arrived while you were at lunch looks exactly like one that never arrived.

In iTerm2, Ghostty and kitty the built-in setting is the cheapest real fix: preferredNotifChannel set to auto posts a genuine desktop notification, and the notifications guide covers its values in full. Everything below is for when that is not enough, or for Terminal.app, where the channel does not exist.

Answer one: a Notification hook that posts a real banner

A hook is a shell command, so you can post a proper macOS notification from any terminal by shelling out. This script uses terminal-notifier for its title, subtitle, sound and - the part that matters with several sessions - a group id, so a second notification about one session replaces the first instead of stacking up.

# brew install terminal-notifier # ~/.claude/claude-waiting.sh - chmod +x after saving #!/bin/bash input=$(cat) field() { printf '%s' "$input" | /usr/bin/jq -r ".$1 // \"\""; } type=$(field notification_type) project=$(basename "$(field cwd)") session=$(field session_id) message=$(field message) # One sound for "asking you", another for "still asking". case "$type" in permission_prompt) sound="Glass"; title="Permission needed" ;; idle_prompt) sound="Ping"; title="Waiting on you" ;; *) exit 0 ;; esac terminal-notifier \ -title "$title" \ -subtitle "$project" \ -message "${message:-Claude Code needs you}" \ -sound "$sound" \ -group "claude-code-$session"

Register it under the Notification event. Hooks are grouped by event name, each group a list of matcher blocks, so the matcher can do the filtering instead of the script:

// ~/.claude/settings.json { "hooks": { "Notification": [ { "matcher": "permission_prompt", "hooks": [ { "type": "command", "command": "$HOME/.claude/claude-waiting.sh" } ] } ] } }

Three things that otherwise cost you an afternoon. jq ships with macOS at /usr/bin/jq, so the absolute path works on a stock Mac and does not depend on the hook inheriting your PATH. The first time terminal-notifier runs macOS asks whether it may post notifications; allow it, or nothing appears and nothing errors. And System Settings → Notifications has to allow it too, with no Focus mode silencing it.

Test it without waiting for a real prompt by feeding it the payload yourself:

printf '%s' \ '{"session_id":"abc","cwd":"'"$PWD"'", "message":"Claude needs permission to use Bash", "notification_type":"permission_prompt"}' \ | ~/.claude/claude-waiting.sh

Leave the matcher out and let the case statement decide instead, and you also get idle_prompt at about the one-minute mark: a useful second nudge for a prompt whose banner you dismissed and then forgot.

Answer two: be asked less often

The best notification is the one that never had to be sent. A prompt for a command you would approve every time is a toll, not a decision, and narrow entries in Claude Code's permissions.allow list remove those permanently - "Bash(npm run test:*)" never prompts again. The list writes itself if you take the "always allow" option on prompts you know you will see again, and starting tasks in plan mode removes most of the rest, because the exploratory phase generates the bulk of them. Both are covered properly in the guide to approving without switching terminal.

Answer three: answer it from outside the terminal

Both answers above solve the same half of the problem: knowing. A banner tells you a session is waiting; you still have to find the window, find the tab, read the command and press a key.

The other half is solvable because of how the mechanism works. A PermissionRequest hook does not fire and forget: it blocks, waiting for a decision, and whatever it prints is the answer. Anything that can reach that blocked process can resolve the prompt where it stands, with no window switch and nothing typed anywhere. You can build that yourself - it is more than a weekend, since the hook has to hold its connection open and something has to own the socket - and the hooks setup guide is where to start if you want the shape of it first.

What CrewTower adds

CrewTower is a native macOS app built on exactly that mechanism. Its hook forwards the event over a local Unix socket and, for a PermissionRequest, holds the connection open while it waits; the app answers over that same socket. The waiting session comes forward in the MacBook notch with the actual command on the card, and you press Allow or Deny there. Nothing is typed into your terminal and it never comes forward: there is deliberately no synthetic-keystroke path in the app. It offers the same three answers the terminal offers - allow once, allow for the rest of this session without writing anything to disk, or always, which writes the durable rule into the project's own settings. Destructive shell commands are flagged on the card before you press. Hooks are configured on first launch, so there is no settings file to write and nothing to unpick later.

Honest limits. What CrewTower can do depends on what the agent exposes: it speaks natively to six (Claude Code, Codex, Cursor, Gemini CLI, Qwen Code, opencode) and monitors twenty-seven, so a visible session does not imply it can answer that session's prompts. Where there is no decision channel the fallback is Jump, which lands on the exact window, tab or split pane. It is $9.99 once for one Mac, macOS 15 or later, 30 days money back, and there is no time-limited trial.

Which one you need

  • One session, in iTerm2, Ghostty or kitty. Set preferredNotifChannel to auto and stop. Everything else is over-engineering.
  • Terminal.app, or you want the banner to name the project. Write the hook above. It is twenty lines and it is yours.
  • Two or more sessions at once. Fix the volume first with an allowlist - notifications scale badly and allowlists scale perfectly - then decide whether the prompts left over are worth a surface that can answer them.

Questions

Which hook event tells me Claude Code is waiting for permission?

Notification with notification_type of permission_prompt. PreToolUse fires for every tool call and is far too noisy; PermissionRequest is the blocking decision hook rather than an alert, though it is the one carrying the command in tool_input.

How long does Claude Code wait before the idle notification?

About a minute of the prompt sitting untouched, at which point a Notification with notification_type of idle_prompt fires. It is a second nudge, not a substitute for handling permission_prompt.

Why do I get no notification at all right now?

Because Claude Code hands notifications to your terminal and your terminal may have nowhere to put them. Terminal.app has no notification channel, so the default is a bell that is easy to miss. iTerm2, Ghostty and kitty do have one, and preferredNotifChannel set to auto uses it.

Can I tell which session the notification came from?

Yes. The payload carries cwd and session_id, so the script above puts the project name in the subtitle and groups per session. This is the main reason to write your own hook rather than rely on the built-in channel.

Can a hook answer the permission request for me?

Yes, and that is the point of PermissionRequest. Returning a decision of allow approves the call without prompting, deny refuses it, and prompt hands it back to the normal flow. Use it for rules you can state precisely, not as a way to say yes to everything.

Do these hooks slow Claude Code down?

A Notification hook runs alongside a prompt that is already waiting for you, so its cost is invisible. Keep the script short and never block in it: a hook that takes seconds makes the agent feel broken.

Know, and answer, without going there

CrewTower puts the waiting session in the MacBook notch with the command on it, and answers over the hook's own socket. One price, one Mac, no subscription.

Get CrewTower for Mac

$9.99 once · macOS 15 or later · 30 days, money back