Native macOS notifications for OpenCode with Ghostty tab focusing.
This project builds a tiny LSUIElement Swift app that owns macOS notifications through UNUserNotificationCenter, plus shell helper scripts that:
- capture the Ghostty terminal target for an OpenCode session
- suppress notifications when you're already on the matching terminal
- dismiss stale notifications when you answer
- focus the matching Ghostty terminal when you click a notification
node-notifier currently relies on a bundled Intel-only terminal-notifier binary. This helper app avoids that dependency and uses native macOS notification APIs directly.
src/
OpenCodeGhosttyNotify.swift
Info.plist
plugin/
index.ts
scripts/
common.sh
opencode-session-start.sh
opencode-notify.sh
opencode-dismiss.sh
install.sh
package.json
This repo now contains a real OpenCode plugin module in plugin/index.ts.
For local development, build it first:
cd ~/Projects/opencode-ghostty-notify
bun install
bun run buildThen point OpenCode at the built package from your config:
mkdir -p ~/.config/opencode/plugins
cat > ~/.config/opencode/plugins/opencode-ghostty-notify.ts <<'EOF'
export { OpenCodeGhosttyNotifyPlugin as NotificationPlugin } from "/Users/carl/Projects/opencode-ghostty-notify/dist/index.js"
EOFOpenCode will load that local plugin file automatically on startup.
If you eventually publish this package to npm, you can switch to the standard config-based plugin loading flow.
The exported plugin names are:
OpenCodeGhosttyNotifyPluginNotificationPlugin
cd ~/Projects/opencode-ghostty-notify
bun install
bun run build
bash install.shRequirements:
- macOS
- Ghostty
- Xcode Command Line Tools
- Bun
sqlite3jqoptional,python3used as a fallback for JSON parsing in shell scripts
The build output lands in:
build/OpenCodeGhosttyNotify.app
State is kept in:
~/Library/Application Support/com.opencode.ghostty.notify/store.db
Two tables are maintained:
sessions: mapssession_idto Ghosttyterminal_uuid,window_id, andtab_idnotifications: maps delivered notification ids back tosession_idso dismiss operations can remove only the matching notifications
Entries older than 24 hours are purged automatically.
Reads JSON from stdin, extracts session_id, and stores the current Ghostty terminal target.
Example:
printf '%s' '{"session_id":"sess-123"}' | ./scripts/opencode-session-start.shReads JSON from stdin, waits a short debounce period, suppresses if the target Ghostty terminal is already focused, then launches the native app.
Accepted top-level keys:
session_idcwdtitlesubtitlemessagenotification_typehook_event_namedelay_secondssound_name
Example:
printf '%s' '{
"session_id":"sess-123",
"cwd":"/Users/carl/.config/opencode",
"message":"Permission required: bash",
"notification_type":"permission.asked"
}' | ./scripts/opencode-notify.shReads JSON from stdin and dismisses notifications previously posted for that session.
printf '%s' '{"session_id":"sess-123"}' | ./scripts/opencode-dismiss.shIf you want to wire it manually, shell out to these scripts at the same lifecycle points you already use internally:
- session creation or first user message:
- call
opencode-session-start.sh
- call
- notification-worthy event:
- call
opencode-notify.sh
- call
- user answers or re-focuses the session:
- call
opencode-dismiss.sh
- call
Minimal example payload:
{
"session_id": "sess-123",
"cwd": "/Users/carl/.config/opencode",
"message": "Waiting for your answer",
"notification_type": "question.asked"
}The bundled OpenCode plugin currently:
- captures Ghostty session targets on
session.created,session.updated, and usermessage.updated - dismisses notifications on user replies and session deletion
- notifies on:
session.idlesession.errorquestion.askedpermission.asked
The plugin uses the shell helpers in this repo, and those helpers in turn launch the native app.
- notification clicks activate Ghostty
- if a Ghostty terminal id was captured for the session, the app focuses that exact terminal
- if only the window/tab ids exist, it falls back to selecting that tab
- if no target is stored, it just activates Ghostty
Runtime logs are written to:
/tmp/opencode-ghostty-notify.log
After building:
bun run check
./build/OpenCodeGhosttyNotify.app/Contents/MacOS/OpenCodeGhosttyNotify --test