Notifications for AI coding agents.
Get a banner + sound when your agent finishes a task or pauses for your approval — step away without missing a beat.
Maintained by StackOne · @StackOneHQ
| Agent | Status |
|---|---|
| Claude Code | ✅ |
| Cursor | ✅ |
| Codex | ✅ |
| Gemini CLI | ✅ † |
| Any hooks-capable agent | ✅ — point it at notify.sh |
† Gemini's tool-permission hook is observability-only — the banner shows the prompt, but the actual Allow / Deny click has to happen in Gemini's terminal. Claude Code, Cursor, and Codex permission events can be approved from the panel directly.
Platforms: macOS — full app with panel, click-to-focus banners, auto-update, quota tracking, voice. Linux (PulseAudio / ALSA / libnotify) and Windows (Git Bash / WSL) get audio + basic notifications via notify.sh only.
Download the latest release from GitHub Releases — pick the .tar.gz matching your Mac's architecture (arm64 for Apple Silicon, x86_64 for Intel), expand, and drag StackNudge.app into ~/Applications/.
CLI shortcut (requires gh):
ARCH=$(uname -m)
gh release download --repo StackOneHQ/stack-nudge --pattern "stack-nudge-*-macos-${ARCH}.tar.gz*"
shasum -a 256 -c stack-nudge-*-macos-${ARCH}.tar.gz.sha256
tar xzf stack-nudge-*-macos-${ARCH}.tar.gz
mv StackNudge.app ~/Applications/
open ~/Applications/StackNudge.appOn first launch, stack-nudge runs a one-screen wizard:
- Detects which agents you have configured (
~/.claude,~/.cursor,~/.gemini). - Wires their hook configs to
notify.sh. - Registers itself + the voice engine as launchd agents so they start at login.
Everything is self-contained inside the .app — no Xcode CLT, no Python, no shell-script bootstrap required. The bundle ships with a portable Python + stackvox (the offline voice engine) already installed. The Kokoro voice model downloads lazily the first time you enable voice notifications.
Subsequent releases install automatically via the in-app auto-updater (Settings → "Update available · vX.Y.Z" when a new release exists).
These platforms get the audio + libnotify path only — no panel, no click-to-focus, no auto-update. The shell installer is what wires notify.sh into agent hooks:
git clone https://github.com/StackOneHQ/stack-nudge.git
cd stack-nudge
./install.shPrerequisites: Python ≥ 3.10 (the bundled voice engine stackvox requires it).
The installer auto-wires hooks for Claude Code (~/.claude/settings.json) and Cursor (~/.cursor/hooks.json). Gemini CLI and Codex are supported through the same notify.sh entry-point, but their hooks must be wired manually — see Manual setup below.
If you're working on stack-nudge itself and want to build from source rather than download a release:
git clone https://github.com/StackOneHQ/stack-nudge.git
cd stack-nudge
./install.shSame script as Linux/Windows; on macOS it additionally builds and installs the panel .app. Requires Xcode CLT. See Development for the inner-loop tools.
Each supported agent has a hooks system. stack-nudge registers these hooks:
| Agent | Event | What happens |
|---|---|---|
| Claude Code | Stop |
Banner when the turn ends |
| Claude Code | PermissionRequest |
Banner when Claude pauses for approval |
| Cursor | stop |
Banner when agent turn ends |
| Gemini CLI | session end | Banner when agent finishes |
The hook calls notify.sh <agent> <event>, which plays a sound and shows a banner via:
- macOS — the native
stack-nudge.app(click-to-focus routes back to your editor) - Linux —
paplay/aplay/notify-send - Windows —
powershell [console]::beep
When you click the banner, stack-nudge.app uses System Events to raise the exact window that triggered the notification — even if you have multiple Cursor or terminal windows open. Supported apps:
- Cursor, VS Code, Zed
- iTerm2, Warp, Ghostty, Terminal.app
If the target app is already in focus when the notification fires, the banner is suppressed and only the sound plays.
Note on Zed: Zed itself doesn't expose an external hook system, so stack-nudge relies on the agent's hooks (e.g.
~/.claude/settings.jsonfor Claude Code) firing from inside Zed's integrated terminal. Click-to-focus and frontmost-window suppression are wired up viaTERM_PROGRAM=zed, which Zed sets automatically.
If you'd rather have your editor focus automatically — no click needed:
export STACKNUDGE_ACTIVATE_IMMEDIATELY=trueAdd that to your shell profile.
If you'd rather not click banners with the mouse, stack-nudge runs a small floating panel that you summon with a hotkey. It has four tabs — Events, Sessions, Usage, and Settings — and is fully keyboard-driven.
The panel is installed and registered as a launchd agent by ./install.sh — no opt-in needed. To run quietly without macOS banners (panel-only):
STACKNUDGE_BANNER=false # optional — suppress macOS banners when using the panelDefault hotkey is cmd+opt+n. Hit it from anywhere to summon the panel; hit it again while focused to hide. Switch tabs with Cmd+1 (Events), Cmd+2 (Sessions), Cmd+3 (Usage), Cmd+4 (Settings) — or click them. Banner and panel can run together, alone, or both off — the sound and voice still fire as passive signals.
Recent nudges in chronological order. Each shows agent, message, project name, time.
| Key | Action |
|---|---|
↑ ↓ |
Move selection |
⏎ |
Approve permission / focus source editor |
O |
Focus source editor without approving |
⌫ |
Dismiss the selected nudge locally |
Esc |
Hide the panel |
When you press ⏎ on a permission event in a VS Code / Cursor terminal pane, stack-nudge walks the editor's accessibility tree to focus the right pane (matched by the agent name in the tab title) before sending Enter — so the approval keystroke lands in the agent's terminal, not whatever was last focused. Falls back gracefully if the pane can't be found.
Live list of running agent processes (claude, gemini, codex — including node-hosted variants like gemini-cli). Polls every 3 seconds while visible. Sessions that exit linger for 30s with ended Ns ago.
| Key | Action |
|---|---|
↑ ↓ |
Move selection |
⏎ |
Focus the session's source terminal |
n |
Rename the selected session inline |
⌫ |
Send SIGTERM to the agent process |
Esc |
Hide the panel |
Reachable from the tab strip or Cmd+3. Renders your Claude Code subscription quota — the same numbers claude /usage shows in the terminal — but always available without typing the command:
- Current session (5-hour rolling window)
- Current week (all models)
- Current week (Opus only) (when your plan has the tier)
- Current week (Sonnet only) (when your plan has the tier)
Bars are color-coded: green below 50%, yellow 50–80%, red 80%+. Reset times shown per tier.
Data is fetched from the same endpoint Claude Code's own statusline uses (/api/oauth/usage), reading your OAuth token from the macOS Keychain. The first time stack-nudge polls you'll see a keychain dialog — click "Always Allow" to grant access (one-time, per release).
Polls every 60 seconds while the panel is visible, or every 5 minutes (STACKNUDGE_USAGE_POLL_MIN) in the background.
When any tier reaches your configured threshold, stack-nudge fires a banner — "Weekly quota at 85% — resets May 17" — once per period per tier, so you get a heads-up before hitting the cap. Configure in Settings → Usage:
- Quota alerts — master switch (default on)
- Alert threshold — 50% / 70% / 80% / 90% / 95% (default 80%)
Reachable from the tab strip or Cmd+4. Keyboard-driven rows for hotkey, banner/voice toggles, sound picks (with preview-on-cycle), voice picker (with preview-on-cycle using a random conversational phrase), speed, quota alert config, and shortcuts to the permissions checker, config file, phrase editor, and quit.
| Key | Action |
|---|---|
↑ ↓ / Tab |
Move selection |
← → |
Cycle the selected row's value (toggles flip, sounds/voices step) |
⏎ |
Activate (toggles flip, action rows fire, hotkey row records a new combo) |
Esc |
Back to events |
The hotkey row records live: press ⏎ on it, press the new combo, and stack-nudge re-registers the global hotkey and writes it to config. If the combo is already grabbed by another app, the previous one stays and an inline error explains why.
stack-nudge also watches ~/.stack-nudge/config for external edits, so changes you make via "Open config file…" or another editor flow back into the running panel without a restart.
When the panel daemon is running, a bell icon appears in your menu bar. The same items you can reach from the in-panel Settings tab are mirrored here for one-click access without summoning the panel:
| Item | What it does |
|---|---|
Hotkey · … |
Shows your current hotkey (info only) |
Show banners |
Toggles STACKNUDGE_BANNER. Enabling fires a confirmation banner. |
Voice notifications |
Toggles STACKNUDGE_VOICE. Enabling speaks "Voice notifications enabled". |
Show panel |
Brings the floating panel up (handy when no events are queued) |
Check permissions… |
Opens the permissions checker (see below) |
Open config file… |
Opens ~/.stack-nudge/config in your default editor |
Quit stack-nudge panel |
Exits the daemon |
Toggles re-read the live config every time the menu opens, so changes you make to ~/.stack-nudge/config directly stay in sync. Banner and voice changes take effect immediately for the next nudge — no daemon restart needed.
The panel needs two privacy grants to fully work:
| Permission | Why |
|---|---|
| Accessibility | Required for AXIsProcessTrusted() to return true — without it, the Enter-to-approve keystroke is silently skipped |
| Automation → System Events | Required for the AppleScript that focuses the right app and window when you act on a nudge |
Open Check permissions… from the menu bar for a live status view. Each row has:
- Reset & prompt — clears the existing TCC entry, then triggers macOS's standard grant dialog
- Settings — opens System Settings to the right pane
The window is set to float above System Settings so you can grant both in one pass without losing it.
stack-nudge's apps are ad-hoc signed, so every rebuild produces a new cdhash. macOS's TCC database binds permissions to that cdhash, which means a fresh build silently invalidates prior grants — even though System Settings still shows the entry as "on". AXIsProcessTrusted() returns false because the running binary's hash no longer matches.
If approval has stopped working after a rebuild, hit Reset & prompt in the permissions checker. It runs tccutil reset, then triggers a fresh dialog bound to the current cdhash.
stack-nudge polls GitHub Releases on launch and every 6 hours. When a newer release exists, the Settings tab gets a small accent dot and an "Update available · vX.Y.Z" row at the top of the list. Click it (or press Enter while it's selected) for a confirmation view with the release notes, then "Update Now" runs the install:
- Downloads the arch-appropriate
.tar.gzartifact for your Mac (~150–200 MB) - Verifies the SHA256 against the sidecar checksum file
- Extracts to a temp directory, strips the
com.apple.quarantinexattr - Atomic-swaps
~/Applications/stack-nudge.appwith the new bundle (keeps the old as.app.oldfor safety) - Runs
launchctl kickstart -k— the current process dies, launchd brings up the new bundle - The new bundle's first launch shows a welcome-style "Updated to vX.Y.Z" screen with the release notes
No source clone, no swiftc rebuild on the user's machine — the new bundle is the already-signed-and-notarized artifact from CI. Updates are fast and don't disturb the user's Xcode CLT or Python install (or lack thereof).
While the StackOne stack-nudge repo is private the auto-updater falls back to your local gh CLI auth (gh api) to read the release metadata. Org members with gh configured see no friction; the actual artifact download uses the release's signed asset URL.
The phrase pools that power Voice notifications can be customised in-app. Settings → "Edit phrases…" opens a keyboard-driven editor where you can:
- Toggle individual built-in phrases on or off (
Space) - Add your own custom phrases (typed inline,
Enterto commit) - Remove custom phrases (
⌫)
Per-pool customisations are stored in ~/.stack-nudge/phrases.user.json and merged with the built-in pools at notification time. Disable a built-in phrase you find too cheery, add ones in your own voice — the same random-selection logic still applies.
stack-nudge uses stackvox, an offline Kokoro-82M TTS engine that speaks notifications aloud with ~13 ms latency. ./install.sh pip-installs it from PyPI into an isolated venv at ~/.stack-nudge/venv — no separate setup needed.
Enable in your config (~/.stack-nudge/config):
STACKNUDGE_VOICE=trueThe voice daemon starts automatically on first notification and is registered as a login item so it stays running across reboots.
Voice fires whenever STACKNUDGE_VOICE=true, alongside the banner and panel surfaces. The frontmost-suppression check still applies — when the source window is already focused, sound, banner, panel post, and voice are all suppressed (you don't need a nudge for the thing you're looking at).
When voice is enabled, the chime is suppressed automatically — voice replaces sound rather than playing alongside it.
Voice messages are picked at random from per-event phrase pools and labelled with the project name (cwd basename, with hyphens/underscores split and a few acronyms expanded — CLI → C L I, MCP → M C P, etc.). For a project called unified-cloud-api you'll hear things like:
- Stop: "unified cloud api is ready for you" / "task complete in unified cloud api" / "output ready in unified cloud api"
- Permission: "unified cloud api requires a decision" / "unified cloud api has a question for you" / "unified cloud api is awaiting approval"
Phrase pools live in ~/.stack-nudge/phrases/ — en.sh, fr.sh, hi.sh, it.sh, pt.sh. The right pool is picked from the configured voice's prefix (af_*/am_*/bf_*/bm_* → en, ff_* → fr, hf_*/hm_* → hi, if_*/im_* → it, pf_*/pm_* → pt) so a French voice speaks French phrasing.
Optional tuning (also in ~/.stack-nudge/config):
STACKNUDGE_VOICE_NAME=af_heart # voice ID (run `~/.stack-nudge/venv/bin/stackvox voices` for the full list)
STACKNUDGE_VOICE_SPEED=1.1 # playback speed (1.0 = normal)| Event | macOS | Linux | Windows |
|---|---|---|---|
| Agent done | Glass.aiff |
freedesktop bell | 800 Hz beep |
| Waiting for approval | Ping.aiff |
freedesktop bell | 1200 Hz beep |
Any file from /System/Library/Sounds/ works on macOS: Basso, Blow, Bottle, Frog, Funk, Glass, Hero, Morse, Ping, Pop, Purr, Sosumi, Submarine, Tink. Override per-event in ~/.stack-nudge/config:
STACKNUDGE_SOUND_STOP=Glass
STACKNUDGE_SOUND_PERMISSION=PingThe Settings tab exposes the same picks with audio preview on each change.
Open the panel (⌘⌥N), go to Settings → Uninstall stack-nudge…, confirm. The app tears down:
- Hook entries in
~/.claude/settings.json,~/.cursor/hooks.json, and~/.gemini/settings.json - The launchd agents (
com.stackonehq.stack-nudge,…-daemon) ~/.stack-nudge/(config,notify.sh, phrases)- Moves
stack-nudge.appto Trash and quits
Settings (config, the cached Kokoro voice model in ~/.cache/huggingface/, your macOS keychain entry for Claude Code) are not touched.
git pull # if you cloned a while back — older uninstall.sh lacks hook cleanup
./uninstall.shSame set of cleanups as the in-app path, useful when the .app isn't reachable or the in-app uninstall failed mid-flight.
Claude Code, Cursor, Codex, and Gemini CLI are auto-wired by the first-launch wizard. For other hooks-capable agents (or to integrate from a custom script), all you need is to invoke notify.sh <agent-label> <event> from wherever your agent emits lifecycle events. <event> should be stop (agent finished a turn) or permission (waiting for approval); <agent-label> can be anything — it just controls the banner title.
Example block in any agent's hooks config:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "$HOME/.stack-nudge/notify.sh my-agent stop", "timeout": 30 }
]
}
]
}
}make build # builds stack-nudge.app into build/
make install # full install (build + copy + register hooks + launchd)
make reload # rebuild + replace installed app + refresh notify.sh + bounce the daemon
make dev # watch sources; auto-reload on .swift / Info.plist / notify.sh / phrase changes
make uninstall # remove app, hooks, launchd agents, ~/.stack-nudge/make dev is the inner-loop tool — leave it running in another terminal, save a Swift file or notify.sh, and the daemon bounces with the new build in ~2 seconds.
Source layout:
panel/— the single persistentstack-nudge.appbinary: hotkey, floating NSPanel, socket listener for incoming events, macOS banner posting viaUNUserNotificationCenter, sessions list, settings, permissions window, auto-updater, quota probeshared/— code shared with the standalone Linux/Windows surfaces (currentlyAppActivator.swift)phrases/— per-language voice phrase pools sourced bynotify.shat hook timenotify.sh— the shell entry-point CC/Cursor/Gemini hooks invoke; on macOS posts events to the running app via Unix-domain socket, on Linux/Windows handles audio + libnotify directly
Swift compiled with swiftc directly. No Xcode, no SPM, no dependencies.
By using stack-nudge or its source code, you agree to the following:
- Use is governed by the MIT License. The software is provided "as is", without warranty of any kind.
- Participation in the project (issues, pull requests, discussions) is subject to the Code of Conduct.
- Suspected security vulnerabilities should be reported privately per SECURITY.md, not via public issues.
- The project is maintained by StackOne; contributions remain under the licence the contributor submits them under (MIT unless otherwise noted).
MIT — see LICENSE for the full text. Copyright © 2026 StackOne Technologies Ltd. and contributors.

