Shared bash library for macOS desktop-notification hooks. Used by parallel Claude Code and Codex CLI dotfiles (mshakeg/claude-dotfiles, mshakeg/codex-dotfiles) so a single canonical implementation drives both, with click-to-focus integration into the mshakeg/vscode-terminal-focus VS Code extension.
Each app's hook framework (Claude Code or Codex CLI) calls a per-app hooks/notify.sh script on events like Stop, PermissionRequest, etc. That script is now a thin wrapper that:
- Sets per-app config (title, icon, env-var prefix, sound + timeout per notification type, transparent-process list).
- Sources
lib.shfrom this repo. - Calls
notify_main "$@".
lib.sh then handles the shared logic:
- Auto-closing macOS notifications via
alerter, sent ascom.apple.ScriptEditor2so banners still appear when VS Code is frontmost. - Throttling fast
Stopturns (configurable via${PREFIX}_MIN_ELAPSED). - Walking the parent process chain to find the integrated terminal's outermost shell PID.
- Looking that PID up in the terminal-focus extension's window registry.
- On click:
code <workspace>to bring the right window to the foreground, then a windowKey-gatedvscode://mshakeg.terminal-focus/focus?pid=…&w=…URI so the right terminal in that window focuses. - Graceful fallback to
code <cwd>if the extension isn't installed or the PID isn't registered. - Opt-in debug logging via
${PREFIX}_DEBUG=1(path:${PREFIX}_DEBUG_LOG, default/tmp/notify-hook.log).
git clone https://github.com/mshakeg/notify-hook.git ~/dev/dotfiles/notify-hook
ln -s ~/dev/dotfiles/notify-hook ~/.notify-hookWrapper scripts source ~/.notify-hook/lib.sh. If you clone the repo elsewhere, adjust the symlink (or set NOTIFY_HOOK_LIB=/path/to/lib.sh in your wrapper).
Dependencies (macOS):
brew install vjeantet/tap/alerter jqIf either is missing, the hook exits silently.
A minimal wrapper looks like:
#!/usr/bin/env bash
set -eu
NOTIFY_APP_TITLE="Claude Code"
NOTIFY_STATE_DIR_NAME="claude-notify"
NOTIFY_ENV_PREFIX="CLAUDE_NOTIFY"
NOTIFY_ICON="$(cd "$(dirname "$0")" && pwd -P)/../icons/claude.png"
NOTIFY_TRANSPARENT_PROCS=(claude)
# Per-type sound + auto-close timeout. macOS ships with bash 3.2, which
# has no associative arrays — define these as functions instead.
notify_sound() {
case "$1" in
perm) echo Funk ;;
stop) echo Bottle ;;
elicit) echo Ping ;;
web) echo Pop ;;
idle) echo ;; # silent
*) echo default ;;
esac
}
notify_timeout() {
case "$1" in
perm|stop|elicit|web) echo 120 ;;
idle) echo 0 ;; # persistent
*) echo 120 ;;
esac
}
source "${NOTIFY_HOOK_LIB:-$HOME/.notify-hook/lib.sh}"
notify_main "$@"| Var | Purpose |
|---|---|
NOTIFY_APP_TITLE |
Popup title; e.g. "Claude Code" or "Codex". |
NOTIFY_STATE_DIR_NAME |
Per-session timestamp directory name (under $TMPDIR). |
NOTIFY_ENV_PREFIX |
Prefix for runtime env vars: ${PREFIX}_MIN_ELAPSED, ${PREFIX}_DEBUG, ${PREFIX}_DEBUG_LOG. |
| Function | Contract |
|---|---|
notify_sound <type> |
Echo the /System/Library/Sounds basename for <type>, or empty for silent. |
notify_timeout <type> |
Echo the auto-close seconds for <type> (0 = persistent until dismissed). |
| Var | Default | Purpose |
|---|---|---|
NOTIFY_ICON |
(none) | Path to a PNG to show in the popup. |
NOTIFY_TRANSPARENT_PROCS |
empty | Array of process basenames find_shell_pid should walk through (e.g. (claude) or (codex)). |
NOTIFY_GROUP_PREFIX |
lowercased app title | alerter --group prefix for notification replacement. |
NOTIFY_EXTENSION_ID |
mshakeg.terminal-focus |
vscode://<ID>/focus URI authority. |
NOTIFY_REGISTRY_PATH |
~/Library/Application Support/code-terminal-focus/windows.json |
Where to read the per-window PID registry from. |
The wrapper invokes notify_main "$@" for both:
notify.sh prompt-start— wire to theUserPromptSubmithook. Records a session timestamp; produces no popup.notify.sh <type> <message>— wire toStop,PermissionRequest, etc. Posts a notification with sound/timeout looked up via<type>.
Type strings are arbitrary — lib.sh doesn't enforce a fixed set. Whatever cases the wrapper's notify_sound() / notify_timeout() functions handle defines the supported types (anything else falls through to the *) default).
export CLAUDE_NOTIFY_DEBUG=1
# … fire a notification …
cat /tmp/notify-hook.logLogs the resolved click_cmd, find_shell_pid result, code invocation, and alerter outcome (@CONTENTCLICKED vs @TIMEOUT). Override the path via ${PREFIX}_DEBUG_LOG=/somewhere/else.
The two dotfiles repos (claude-dotfiles, codex-dotfiles) are versioned independently — they couldn't share files via a single repo without one consuming the other. Pulling the shared notification logic out into notify-hook lets both depend on the same upstream and means a bugfix or feature lands once, not twice.