vde-notifier
v0.1.6
Published
tmux focus notification CLI for macOS
Downloads
285
Maintainers
Readme
vde-notifier
vde-notifier is a tmux-aware notification CLI for macOS. It surfaces long-running pane completions, plays a sound, and returns you to the exact session/window/pane with a single notification click.
Quick Start
- Install prerequisites:
- macOS 14 or later
tmux,vde-notifier-app(default notifier)- Node.js 22+ or Bun 1.1+
pnpm
vde-notifier is published as a macOS-only npm package (os: darwin, engines.node: >=22).
# install default notifier backend
brew tap yuki-yano/vde-notifier
brew install --cask yuki-yano/vde-notifier/vde-notifier-app
# optional fallback notifier backends
brew install terminal-notifier
brew install yuki-yano/swiftdialog/swift-dialog
# (or download the official pkg from https://github.com/swiftDialog/swiftDialog/releases)- Run the CLI without installing (choose one):
bun x vde-notifier@latest # recommended
npx vde-notifier@latest
pnpm dlx vde-notifier@latest(Optional) Install globally if you prefer a persistent binary:
bun install --global vde-notifier@latest
npm install -g vde-notifier@latest
pnpm add -g vde-notifier@latest- Inside tmux, trigger a notification when your pane finishes work:
vde-notifier --title "Build finished" --message "webpack completed"- Click the macOS notification. vde-notifier will:
- Play the selected sound (default: Glass)
- Bring your terminal frontmost
- Focus the matching tmux client/session/window/pane
CLI Options
--title <string>: Override the notification title. Defaults to[session] window.pane (%paneId).--message <string>: Override the notification body. Defaults tocmd: <paneCurrentCommand> | tty: <clientTTY>.--sound <name>: macOS system sound (for example,Glass,Ping). UseNonefor silence.--codex: Consume Codex-style JSON (see below) from a trailing argument,CODEX_NOTIFICATION_PAYLOAD, or stdin (in that priority order) and build the notification from it.--skip-codex-subagent: Skip sending notifications when Codex payload belongs to a subagent turn (thread-idlookup from~/.codex/sessions).--skip-codex-non-interactive: Skip sending notifications for Codex non-interactive turns (source: "exec"in~/.codex/sessions, such ascodex exec/codex review).--claude: Consume Claude Code JSON piped on stdin (supportstranscript_pathto pull the latest assistant reply).--skip-claude-non-interactive: Skip sending notifications when Claude non-interactive (-p/--print --output-format json) payloads are detected.--terminal <profile>: Force a terminal profile (alacritty, wezterm, ghostty, etc.).--term-bundle-id <bundleId>: Override the bundle identifier when auto detection is insufficient.--notifier <terminal-notifier|swiftdialog|vde-notifier-app>: Switch the notification backend. Defaults tovde-notifier-app.--dry-run: Skips sending a notification. Combine with--verboseto print the gathered tmux metadata and focus command.--verbose: Emits JSON logs describing notify and focus stages.--log-file <path>: Appends the same JSON diagnostics to the given file (one JSON object per line). Also propagates to focus-mode invocations.-- <command> [args...]: After sending the notification, execute another command and forward runtime arguments (for example, Codex payload JSON) to it.--help,-h: Show usage.--version,-v: Show CLI version.
Short option bundling (for example, -hv) is intentionally unsupported.
When --notifier swiftdialog is selected, vde-notifier plays the requested sound locally and then sends dialog --notification ... with a primary action wired to the focus command. Clicking the notification will restore the tmux pane.
When --notifier vde-notifier-app is selected, vde-notifier calls the local Swift agent (vde-notifier-app notify ...) with action executable and arguments so notification clicks can restore the tmux pane without shell interpolation.
Using vde-notifier-app via Cask
- Install the app:
brew tap yuki-yano/vde-notifier
brew install --cask yuki-yano/vde-notifier/vde-notifier-app- Verify runtime health:
vde-notifier-app doctor
vde-notifier-app agent status- Send notifications through the Swift backend (default):
vde-notifier --title "Build finished" --message "Done" --sound Ping- Optional smoke test for the app binary directly:
vde-notifier-app notify --title "swift smoke" --message "click me" --sound Ping --action-exec /usr/bin/say --action-arg "clicked"Environment overrides:
VDE_NOTIFIER_TERMINAL=alacrittysets the default terminal profile when--terminalis omitted. Valid aliases:terminal,apple-terminal,mac-terminal,iterm,iterm2,alacritty,kitty,wezterm,hyper,ghostty(non-matching values fall back to Terminal.app).VDE_NOTIFIER_LOG_FILE=/path/to/diagnostics.logmirrors--log-fileso every run writes diagnostics even without passing the CLI flag.
Typical Workflow
- Export a preferred terminal profile:
export VDE_NOTIFIER_TERMINAL=alacritty- From tmux, run a long task and send a notification on completion:
make build && vde-notifier --title "Build" --message "Done" --sound Ping- When the notification appears, click it. Even if Alacritty already has focus, vde-notifier will:
- Run tmux
switch-client,select-window,select-pane - Activate Alacritty through macOS Automation and make sure it is frontmost
- Preserve IME focus for immediate typing
Using from AI Agents (Claude Code, Codex, etc.)
Many hosted IDE agents run inside tmux. You can add a notification step after long tasks so the human operator gets paged immediately:
Agent JSON Input
vde-notifier hydrates notifications from agent payloads in two ways:
--codex: pass a Codex-style JSON payload as the final argument (the format used by hosted Codex agents). You can also preload the same JSON viaCODEX_NOTIFICATION_PAYLOAD; if neither is present, stdin is used.--claude: pipe Claude Code's JSON payload to stdin. If the payload containstranscript_path, vde-notifier opens the referenced transcript JSONL file and uses the latest assistant message.
When using command chaining (-- <command> [args...]) with --codex, vde-notifier treats the last forwarded argument as the Codex payload JSON, sends the notification first, then executes the chained command with the same forwarded arguments.
Codex notifications always use the repository-scoped title Codex: <repo-name>, ignoring payload-provided titles. Claude notifications fall back to Claude: <repo-name> when no explicit title is supplied.
If either payload is malformed JSON, the command exits with a non-zero status.
For either flag the CLI looks for:
- Title (
notification-title,notification_title,title) - The most recent assistant message (from
notification-message,notification_message,last-assistant-message,message,messages,transcript, or Claude transcripts) - Sound (
sound, respectingnone,default, or full paths such as/System/Library/Sounds/Ping.aiff) - Codex thread id (
thread-id,thread_id,threadId) to support--skip-codex-subagent/--skip-codex-non-interactive(via~/.codex/sessionslookup) - Claude non-interactive result payload shape (
type: "result",subtype,result) to support--skip-claude-non-interactive
To enable automatic notifications from Codex CLI/agents, add the following to ~/.codex/config.toml:
notify = ["bun", "x", "vde-notifier@latest", "--codex"]If you also want to run another command from the same notify hook while keeping the same Codex payload argument:
notify = ["bun", "x", "vde-notifier@latest", "--codex", "--", "other-command"]At runtime this behaves like:
bun x vde-notifier@latest --codex -- other-command '{"message":"..."}'vde-notifier sends the notification first, then runs other-command '{"message":"..."}'.
For Claude Code (Claude Desktop) projects, add a Stop hook to ~/.config/claude/settings.json so every long-running tool run triggers a notification when it finishes:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bun x vde-notifier@latest --claude"
}
]
}
]
}
}Release Flows (GitHub Actions)
NPM publish and app/cask release are separated.
NPM publish
Workflow:
.github/workflows/publish.yml
Trigger:
- Push a tag with
npm-v*(example:npm-v0.1.1) - The tag version must match
package.json(for example,npm-v0.1.1with"version": "0.1.1").
Example:
git tag npm-v0.1.1
git push origin npm-v0.1.1publish.yml also verifies that the target version is not already on npm and publishes with provenance (npm publish --provenance).
App/cask release
vde-notifier-app cask distribution expects this fixed asset name on yuki-yano/vde-notifier releases:
VdeNotifierApp.app.tar.gz
Release automation is defined in:
.github/workflows/release-vde-notifier-app.yml
The workflow:
- Builds the Swift app release asset.
- Uploads/replaces
VdeNotifierApp.app.tar.gzon the target release tag. - Computes SHA256 for the uploaded asset.
- Sends
repository_dispatchtoyuki-yano/homebrew-vde-notifierso caskversionandsha256are updated automatically.
Repository secret required in yuki-yano/vde-notifier:
HOMEBREW_TAP_DISPATCH_TOKEN: token with write permission toyuki-yano/homebrew-vde-notifier.
Standard release flow
- Create and push an app tag:
git tag app-v0.1.1
git push origin app-v0.1.1- Wait for
release-vde-notifier-appworkflow to complete. - Confirm the release contains
VdeNotifierApp.app.tar.gz. - Confirm
yuki-yano/homebrew-vde-notifierreceives anupdate-caskrun and commits updatedversion/sha256.
Manual rerun flow
If you need to regenerate the asset for an existing tag, run the workflow manually (workflow_dispatch) and set:
tag:app-v0.1.1(existing app tag)
If tap update does not run automatically, manually trigger update-cask.yml in
yuki-yano/homebrew-vde-notifier with the same app version (without app-v)
and the SHA256 of VdeNotifierApp.app.tar.gz.
Local pre-check
Before tagging, you can build the exact release asset locally:
pnpm run swift:release-assetTroubleshooting
- No sound: Ensure the sound name matches a file in
/System/Library/Sounds/and is not set toNone. vde-notifier-appcommand is missing: Install the default notifier backend:brew tap yuki-yano/vde-notifier && brew install --cask yuki-yano/vde-notifier/vde-notifier-app- Notification click does nothing: Run with
--verboseto inspect payload and focus command. Confirmosascriptautomation permission is granted. vde-notifier-app doctorstaysnotDetermined: Rebuild the app bundle (pnpm run swift:app) and verify the signature identifier (codesign -dv --verbose=4 build/VdeNotifierApp.app 2>&1 | rg '^Identifier=') iscom.yuki-yano.vde-notifier-app.agent.- Slow focus switch: By default tmux commands run first, then the terminal is frontmost. If delays persist, check that Notification Center closes promptly and that tmux socket is reachable.
- Running from
bunx dlxor AI agents: If launched via package runners, vde-notifier reuses the currentprocess.execPathso focus mode can start without PATH access.
Development Notes (Optional)
- Install dependencies:
pnpm install - Lint:
pnpm run lint - Test:
pnpm run test - Package dry-run check:
pnpm run pack:check - Build:
pnpm run build - Watch build:
pnpm run dev - Swift backend tests:
pnpm run swift:test - Swift backend build:
pnpm run swift:build - Build app bundle:
pnpm run swift:app
