Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ClaudeCodeNotify

Native macOS notification system for Claude Code. Get notified when Claude finishes a task or needs your permission — click the notification to jump straight back to your terminal.

macOS Swift License

Features

  • Native macOS notifications via UNUserNotificationCenter — no third-party dependencies
  • Click-to-focus: clicking a notification jumps straight to your terminal with full keyboard focus
  • Exact-tab focus (Ghostty): on Ghostty, the click lands on the exact tab running that Claude session — even with several tabs open in the same folder — by matching the tab's unique terminal UUID
  • Differentiated alerts: distinct sounds and messages for "task complete" vs "permission needed"
  • Smart suppression: skips notifications when any terminal is already focused — no noise about what you're looking at
  • Claude icon: notifications display the Claude app icon
  • Daemon mode: runs persistently in the background, watching a trigger file — no process spawning per notification
  • Auto-detects your terminal: focuses whichever terminal you're actually using (Ghostty, Warp, iTerm2, Kitty, Alacritty, WezTerm, Hyper, Terminal.app) — no configuration needed, even with several terminals open at once
  • Lightweight: compiled Swift binary, background daemon with zero CPU when idle

Requirements

  • macOS 14+ (tested on macOS 26 Tahoe)
  • Swift compiler (swiftc) — included with Xcode Command Line Tools
  • Claude.app installed (for the notification icon)
  • Claude Code CLI

Quick Install

git clone https://github.com/IARFLOW/ClaudeCodeNotify.git
cd ClaudeCodeNotify
bash scripts/install.sh

When macOS asks to allow notifications, click Allow.

How It Works

ClaudeCodeNotify runs as a background daemon that watches the file ~/.claude/notify-trigger. Claude Code hooks write notification data to this file, and the daemon picks it up instantly, sends the notification, and clears the file.

The trigger file format is (the last three fields are optional and backward compatible):

message|subtitle|sound|category|term|cwd|tabId

For example: Finished and waiting|Done|Glass.aiff|STOP|ghostty|/Users/me/project|<uuid>

When you click a notification, it activates your terminal with full keyboard focus — no mouse click needed.

The daemon detects which terminal you're using automatically: it tracks the most recently active terminal application, so a click always returns to the terminal you were last working in — even if you have several terminals open, or switched to a browser while waiting. No need to hardcode a terminal.

Exact-tab focus on Ghostty. Ghostty gives each terminal a stable, unique id (UUID) and exposes it over AppleScript. A SessionStart hook captures the UUID of the tab where you launched Claude (at that moment it's the focused tab) and saves it under /tmp/claude-ghostty/<session_id>. The Stop / Notification hooks read it back and pass it in the trigger. On click, the daemon runs focus (first terminal whose id is "<uuid>"), landing on the precise tab — even if several tabs share the same directory. If the UUID is gone (tab closed) it falls back to matching the working directory, then to plain app activation. Never worse than app-level focus.

Configure Claude Code Hooks

Add the following to your ~/.claude/settings.json. The SessionStart hook captures the Ghostty tab's UUID; the Stop and Notification hooks read it back and include it in the trigger (they parse stdin with jq):

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "input=$(cat); sid=$(printf '%s' \"$input\" | jq -r '.session_id // empty'); if [ \"$TERM_PROGRAM\" = \"ghostty\" ] && [ -n \"$sid\" ]; then mkdir -p /tmp/claude-ghostty; tid=$(osascript -e 'tell application \"Ghostty\" to return id of focused terminal of selected tab of first window' 2>/dev/null); printf '%s' \"$tid\" > \"/tmp/claude-ghostty/$sid\"; fi"
          }
        ]
      }
    ],
    "Notification": [
      {
        "matcher": "permission_prompt",
        "hooks": [
          {
            "type": "command",
            "command": "input=$(cat); cwd=$(printf '%s' \"$input\" | jq -r '.cwd // empty'); sid=$(printf '%s' \"$input\" | jq -r '.session_id // empty'); tid=$(cat \"/tmp/claude-ghostty/$sid\" 2>/dev/null); printf 'Needs your permission to continue|Action Required|Basso.aiff|PERMISSION_PROMPT|%s|%s|%s' \"$TERM_PROGRAM\" \"$cwd\" \"$tid\" > ~/.claude/notify-trigger"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "input=$(cat); cwd=$(printf '%s' \"$input\" | jq -r '.cwd // empty'); sid=$(printf '%s' \"$input\" | jq -r '.session_id // empty'); tid=$(cat \"/tmp/claude-ghostty/$sid\" 2>/dev/null); printf 'Finished and waiting for your response|Done|Glass.aiff|STOP|%s|%s|%s' \"$TERM_PROGRAM\" \"$cwd\" \"$tid\" > ~/.claude/notify-trigger"
          }
        ]
      }
    ]
  }
}

Note: The Stop hook fires every time Claude finishes responding. The Notification hook with permission_prompt matcher fires when Claude needs your approval to proceed. The SessionStart hook only does anything under Ghostty; on other terminals it's a no-op and the click falls back to app-level focus.

Requirements for exact-tab focus: jq on your PATH, and macOS Automation permission. The first time a hook or the daemon scripts Ghostty, macOS prompts "… wants to control Ghostty" — click Allow (also under System Settings → Privacy & Security → Automation).

Terminal selection

By default you don't need to configure anything — the daemon auto-detects the terminal you're using. It recognizes Ghostty, Warp, iTerm2, Kitty, Alacritty, WezTerm, Hyper, and Terminal.app, and resolves which one to focus in this order:

  1. CLAUDE_NOTIFY_TERMINAL environment variable, if you set one (explicit override)
  2. The terminal that was most recently active (tracked live as you switch apps)
  3. The first recognized terminal currently running
  4. Warp, as a last-resort fallback

To force a specific terminal — for example if you run an unlisted one, or always want clicks to land on one terminal — set its bundle identifier:

# Add to your ~/.zshrc or ~/.bashrc
export CLAUDE_NOTIFY_TERMINAL="com.googlecode.iterm2"
Common terminal bundle identifiers
Terminal Bundle ID
Ghostty com.mitchellh.ghostty
Warp dev.warp.Warp-Stable
iTerm2 com.googlecode.iterm2
Terminal.app com.apple.Terminal
Kitty net.kovidgoyal.kitty
Alacritty org.alacritty
WezTerm com.github.wez.wezterm
Hyper co.zeit.hyper

To find any app's bundle ID:

defaults read /Applications/YourTerminal.app/Contents/Info.plist CFBundleIdentifier

Available macOS sounds

Basso, Blow, Bottle, Frog, Funk, Glass, Hero, Morse, Ping, Pop, Purr, Sosumi, Submarine, Tink

Auto-start with LaunchAgent

To keep the daemon running automatically, create ~/Library/LaunchAgents/com.iarflow.claudecodenotify.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.iarflow.claudecodenotify</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Applications/ClaudeCodeNotify.app/Contents/MacOS/ClaudeCodeNotify</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
</dict>
</plist>

Then load it:

launchctl load ~/Library/LaunchAgents/com.iarflow.claudecodenotify.plist

Tips

Make notifications persist until dismissed

By default, macOS banners disappear after a few seconds. To make them stay:

  1. Open System Settings > Notifications
  2. Find Claude Code in the app list
  3. Change notification style from Banners to Alerts

Troubleshooting

Notifications appear but clicking them does nothing

If click-to-focus stops activating your terminal even though notifications are still being delivered, macOS's internal notification router (usernoted) may have a stale delegate reference. Reset it with:

killall usernoted

usernoted respawns automatically in under a second with no data loss. This is a generic macOS fix, not specific to ClaudeCodeNotify — it's worth knowing for any notification-based app.

Notifications don't appear at all

  1. Check the daemon is running:

    pgrep -fl ClaudeCodeNotify

    There should be exactly one process. If there are zero, load the LaunchAgent. If there are more than one, something went wrong with install — run pkill -f ClaudeCodeNotify and reinstall.

  2. Test the daemon directly, bypassing Claude Code hooks:

    echo 'Test|From terminal|Glass.aiff|IDLE_PROMPT' > ~/.claude/notify-trigger

    Run this from an app other than your terminal (otherwise the built-in focus suppression kicks in). If you get a notification, the daemon is healthy and the issue is in your hook configuration.

  3. Verify the app has notification permission in System Settings → Notifications → Claude Code.

Uninstall

launchctl unload ~/Library/LaunchAgents/com.iarflow.claudecodenotify.plist
rm ~/Library/LaunchAgents/com.iarflow.claudecodenotify.plist
rm -rf /Applications/ClaudeCodeNotify.app

Then remove the hooks section from ~/.claude/settings.json.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages