If you have ever run a long Claude Code session and found yourself switching back to VS Code every 30 seconds to check whether it needs your input, this is for you.
The Problem
Claude Code is great at working autonomously, but it regularly needs to pause and ask for something. It might be a permission request before running a tool, or a plan it wants you to review before proceeding. The problem is that nothing alerts you when this happens. You are just expected to be watching.
In practice this means one of two things. Either you stay glued to the editor and lose the whole point of having an AI agent handle things in the background, or you walk away and come back minutes later to find Claude has been sitting idle waiting for your approval.
There is no middle ground in the default setup. No sound. No badge. No popup. Nothing.
What We Actually Want
The ideal experience is simple. You kick off a task, go do something else, and get a notification the moment Claude needs you. Same as any other async workflow tool.
macOS has a perfectly good notification system. VS Code supports extensions that can trigger it. Claude Code supports hooks that run shell commands at key points in its lifecycle. Connecting these three things is all it takes.
The Solution
We use terminal-notifier, a small command line tool that posts native macOS notifications, and wire it into Claude Code via the hooks configuration in ~/.claude/settings.json.
The result looks exactly like this:
Three events covered:
- Permission required — Claude wants to run a tool and is waiting for you to allow it
- Plan ready for review — Claude has drafted a plan and is holding for your approval
- Task completed — Claude finished a turn and the result is ready
Clicking any notification brings VS Code back into focus.
Setup
Step 1: Install terminal-notifier
brew install terminal-notifier
The first time a notification fires, macOS will prompt you to allow notifications from terminal-notifier in System Settings. Accept it.
Step 2: Add hooks to ~/.claude/settings.json
Open or create the file and merge in the following. If you already have settings there, add only the hooks block rather than replacing the whole file.
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/opt/homebrew/bin/terminal-notifier -title \"Claude Code\" -message \"Task completed in $(basename \"${CLAUDE_PROJECT_DIR:-$PWD}\")\" -activate com.microsoft.VSCode -contentImage \"/Applications/Visual Studio Code.app/Contents/Resources/Code.icns\" 2>/dev/null || true",
"async": true,
"timeout": 10
}
]
}
],
"PermissionRequest": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "tool=$(jq -r '.tool_name // empty'); proj=$(basename \"${CLAUDE_PROJECT_DIR:-$PWD}\"); if [ \"$tool\" = \"ExitPlanMode\" ]; then msg=\"Plan ready for review in $proj\"; else msg=\"Permission required in $proj\"; fi; /opt/homebrew/bin/terminal-notifier -title \"Claude Code\" -message \"$msg\" -activate com.microsoft.VSCode -contentImage \"/Applications/Visual Studio Code.app/Contents/Resources/Code.icns\" 2>/dev/null || true",
"async": true,
"timeout": 10
}
]
}
]
}
Step 3: Restart VS Code
Reload the window so the hooks are picked up. After that you are done.
Things That Tripped Us Up
Use the absolute path to terminal-notifier. VS Code launched from the Dock does not inherit your shell PATH, which means Homebrew binaries are invisible to it. Hooks that call terminal-notifier without the full path will fail silently, leaving you with no notifications and no error to debug. Always use /opt/homebrew/bin/terminal-notifier.
Use CLAUDE_PROJECT_DIR instead of $PWD. Hooks are not guaranteed to run with the working directory set to your project. The CLAUDE_PROJECT_DIR environment variable is the reliable way to get the project name into your notification message.
Plan mode approval goes through PermissionRequest, not PreToolUse. If you were hoping to catch AskUserQuestion or ExitPlanMode via PreToolUse, it does not work. Those events do not fire that hook. The PermissionRequest hook covers plan reviews by checking the tool_name field from stdin and switching the message accordingly.
The Stop hook fires on every response, including quick replies. If you find the "Task completed" notification too noisy during interactive back and forth, remove the Stop block and keep only PermissionRequest. The permission and plan notifications are the high value ones anyway.
Why This Matters
Claude Code is at its best when you can give it a task and actually step away. Notifications are what make that possible. Without them you are not using an agent, you are just using a fancy autocomplete that requires constant supervision.
This setup takes about two minutes to configure and genuinely changes how you work with it.
How Are You Handling This?
This is one of those small quality of life improvements that makes a bigger difference than you expect. But there are probably other ways to solve the same problem, and some of them might be better.
Are you using a different notification tool? A custom shell script? Something built into your editor setup? Drop it in the comments below. Always curious to see how other people approach the same friction points.

Top comments (0)