OpenCode plugin for desktop notifications and sounds on permission, completion, and error events.
818
stars
205
commits
TypeScript
primary language
Jul 19, 2026
updated
OpenCode plugin that plays sounds and sends system notifications when permission is needed, generation completes, errors occur, or the question tool is invoked. Works on macOS, Linux, and Windows.
Add this to your opencode.json:
{
"plugin": ["@mohak34/opencode-notifier@latest"]
}
Restart OpenCode. Done.
You'll get notified when:
There's also subagent_complete for when subagents finish, and user_cancelled for when you press ESC to abort -- both are silent by default so you don't get spammed.
macOS: Nothing to do, works out of the box. Shows the Script Editor icon.
Linux: Should work if you already have a notification system setup. If not install libnotify:
sudo apt install libnotify-bin # Ubuntu/Debian
sudo dnf install libnotify # Fedora
sudo pacman -S libnotify # Arch
For sounds, you need one of: paplay, aplay, mpv, or ffplay
Windows: Works out of the box. But heads up:
.wav files work (not mp3)C:/Users/You/sounds/alert.wav not ~/WSL: It's recommeneded to set customIconPath pointing to a file on Windows filesystem
due to issues with path translation (can be copied from logos folder from this repository).
This path will be passed down to snoretoast-*.exe
In opencode-notifier.json config:
"showIcon": true,
"customIconPath": "C:\\Users\\jhon\\Documents\\opencode-logo-dark.png",
Create ~/.config/opencode/opencode-notifier.json with the defaults:
{
"sound": true,
"notification": true,
"bell": false,
"timeout": 5,
"showProjectName": true,
"showFullPath": false,
"showSessionTitle": false,
"showIcon": true,
"customIconPath": null,
"suppressWhenFocused": true,
"enableOnDesktop": false,
"notificationSystem": "osascript",
"suppressGhosttySound": false,
"linux": {
"grouping": false
},
"minDuration": 0,
"command": {
"enabled": false,
"path": "/path/to/command",
"args": ["--event", "{event}", "--message", "{message}"],
"minDuration": 0
},
"events": {
"permission": { "sound": true, "notification": true, "command": true, "bell": false },
"complete": { "sound": true, "notification": true, "command": true, "bell": false },
"subagent_complete": { "sound": false, "notification": false, "command": true, "bell": false },
"error": { "sound": true, "notification": true, "command": true, "bell": false },
"question": { "sound": true, "notification": true, "command": true, "bell": false },
"user_cancelled": { "sound": false, "notification": false, "command": true, "bell": false },
"plan_exit": { "sound": true, "notification": true, "command": true, "bell": false },
"session_started": { "sound": true, "notification": false, "command": true, "bell": false },
"user_message": { "sound": true, "notification": false, "command": true, "bell": false },
"client_connected": { "sound": true, "notification": false, "command": true, "bell": false }
},
"messages": {
"permission": "Session needs permission: {sessionTitle}",
"complete": "Session has finished: {sessionTitle}",
"subagent_complete": "Subagent task completed: {sessionTitle}",
"error": "Session encountered an error: {sessionTitle}",
"question": "Session has a question: {sessionTitle}",
"user_cancelled": "Session was cancelled by user: {sessionTitle}",
"plan_exit": "Plan ready for review: {sessionTitle}",
"session_started": "Session started: {sessionTitle}",
"user_message": "User sent a message: {sessionTitle}",
"client_connected": "OpenCode connected"
},
"sounds": {
"permission": null,
"complete": null,
"subagent_complete": null,
"error": null,
"question": null,
"user_cancelled": null,
"plan_exit": null,
"session_started": null,
"user_message": null,
"client_connected": null
},
"volumes": {
"permission": 1,
"complete": 1,
"subagent_complete": 1,
"error": 1,
"question": 1,
"user_cancelled": 1,
"plan_exit": 1,
"session_started": 1,
"user_message": 1,
"client_connected": 1
}
}
{
"sound": true,
"notification": true,
"bell": false,
"timeout": 5,
"showProjectName": true,
"showFullPath": false,
"showSessionTitle": false,
"showIcon": true,
"suppressWhenFocused": true,
"enableOnDesktop": false,
"notificationSystem": "osascript",
"suppressGhosttySound": false
}
sound - Turn sounds on/off (default: true)notification - Turn notifications on/off (default: true)bell - Emit terminal BEL (\x07) on events (default: false). Behavior depends on your terminal/WM settingstimeout - How long notifications show in seconds, Linux only (default: 5)showProjectName - Show folder name in notification title (default: true)showFullPath - Show full absolute path instead of folder name in notification title and {projectName} token (default: false). When true, shows OpenCode (/home/user/projects/myapp) instead of OpenCode (myapp)showSessionTitle - Include the session title in notification messages via {sessionTitle} placeholder (default: false)showIcon - Show OpenCode icon, Windows/Linux only (default: true)customIconPath - Path to a custom icon for notifications. Useful on WSL where Windows paths are needed (default: null)suppressWhenFocused - Skip notifications and sounds when the terminal is the active window (default: true). See Focus detection for platform detailsenableOnDesktop - Run the plugin on Desktop and Web clients (default: false). When false, the plugin only runs on CLI. Set to true if you want notifications/sounds/commands on Desktop/Web — useful if you want custom commands (Telegram, webhooks) but don't care about built-in notificationsnotificationSystem - macOS only: "osascript", "node-notifier", or "ghostty" (default: "osascript"). Use "ghostty" if you're running Ghostty terminal for native OSC 9 notificationssuppressGhosttySound - macOS only: when true with notificationSystem: "ghostty", skips the plugin's sound to avoid duplicating macOS Notification Center's default sound (default: false)minDuration - Suppress complete and subagent_complete notifications when session finishes faster than this many seconds (default: 0). See Minimum duration thresholdlinux.grouping - Linux only: replace notifications in-place instead of stacking (default: false). Requires notify-send 0.8+Control each event separately:
{
"events": {
"permission": { "sound": true, "notification": true, "command": true, "bell": false },
"complete": { "sound": true, "notification": true, "command": true, "bell": false },
"subagent_complete": { "sound": false, "notification": false, "command": true, "bell": false },
"error": { "sound": true, "notification": true, "command": true, "bell": false },
"question": { "sound": true, "notification": true, "command": true, "bell": false },
"user_cancelled": { "sound": false, "notification": false, "command": true, "bell": false },
"plan_exit": { "sound": true, "notification": true, "command": true, "bell": false },
"session_started": { "sound": true, "notification": false, "command": true, "bell": false },
"user_message": { "sound": true, "notification": false, "command": true, "bell": false },
"client_connected": { "sound": true, "notification": false, "command": true, "bell": false }
}
}
user_cancelled fires when you press ESC to abort a session. It's silent by default so intentional cancellations don't trigger error alerts. Set sound or notification to true if you want confirmation when cancelling.
session_started fires when a new top-level session is created. user_message fires when a user message is submitted in a top-level session. client_connected fires shortly after the plugin initializes and is best-effort (there is no dedicated SDK connection event from plugin context).
The command property controls whether the custom command (see Custom commands) runs for that event. Defaults to true for all events. Set it to false to suppress the command for specific events without disabling it globally.
bell is terminal-driven and may be audible, visual, both, or ignored depending on your terminal setup. Quick check: printf '\a'.
Or use true/false for both:
{
"events": {
"complete": false
}
}
Customize the notification text:
{
"messages": {
"permission": "Session needs permission: {sessionTitle}",
"complete": "Session has finished: {sessionTitle}",
"subagent_complete": "Subagent task completed: {sessionTitle}",
"error": "Session encountered an error: {sessionTitle}",
"question": "Session has a question: {sessionTitle}",
"user_cancelled": "Session was cancelled by user: {sessionTitle}",
"plan_exit": "Plan ready for review: {sessionTitle}",
"session_started": "Session started: {sessionTitle}",
"user_message": "User sent a message: {sessionTitle}",
"client_connected": "OpenCode connected"
}
}
Messages support placeholder tokens that get replaced with actual values:
{sessionTitle} - The title/summary of the current session (e.g. "Fix login bug"){agentName} - Subagent name extracted from session titles with (@name subagent) suffix (e.g. builder, codebase-researcher), empty for non-subagent sessions{projectName} - The project folder name{timestamp} - Current time in HH:MM:SS format (e.g. "14:30:05"){turn} - Global notification counter that persists across restarts (e.g. 1, 2, 3). Stored in ~/.config/opencode/opencode-notifier-state.jsonWhen showSessionTitle is false, {sessionTitle} is replaced with an empty string. Any trailing separators (: , -, |) are automatically cleaned up when a placeholder resolves to empty.
To disable session titles in messages without changing showSessionTitle, just remove the {sessionTitle} placeholder from your custom messages.
The {timestamp} and {turn} placeholders also work in custom command args.
Use your own sound files:
{
"sounds": {
"permission": "/path/to/alert.wav",
"complete": "/path/to/done.wav",
"subagent_complete": "/path/to/subagent-done.wav",
"error": "/path/to/error.wav",
"question": "/path/to/question.wav",
"user_cancelled": "/path/to/cancelled.wav",
"plan_exit": "/path/to/plan-ready.wav",
"session_started": "/path/to/session-started.wav",
"user_message": "/path/to/user-message.wav",
"client_connected": "/path/to/client-connected.wav"
}
}
Platform notes:
Set per-event volume from 0 to 1:
{
"volumes": {
"permission": 0.6,
"complete": 0.3,
"subagent_complete": 0.15,
"error": 1,
"question": 0.7,
"user_cancelled": 0.5,
"plan_exit": 0.6,
"session_started": 0.35,
"user_message": 0.2,
"client_connected": 0.45
}
}
0 = mute, 1 = full volume0..1 are clamped automaticallyRun your own script when something happens. Use {event}, {message}, {sessionTitle}, {agentName}, {projectName}, {timestamp}, and {turn} as placeholders:
{
"command": {
"enabled": true,
"path": "/path/to/your/script",
"args": ["{event}", "{message}"],
"minDuration": 10
}
}
enabled - Turn command on/offpath - Path to your script/executableargs - Arguments to pass, can use {event}, {message}, {sessionTitle}, {agentName}, {projectName}, {timestamp}, and {turn} tokensminDuration - Skip if response was quick, avoids spam (seconds){
"command": {
"enabled": true,
"path": "/bin/bash",
"args": [
"-c",
"echo '[{event}] {message}' >> /tmp/opencode.log"
]
}
}
osascript (default): Reliable but shows Script Editor icon
{
"notificationSystem": "osascript"
}
node-notifier: Shows OpenCode icon but might miss notifications sometimes
{
"notificationSystem": "node-notifier"
}
NOTE: If you go with node-notifier and start missing notifications, just switch back or remove the option from the config. Users have reported issues with using node-notifier for receiving only sounds and no notification popups.
If you're using Ghostty terminal, you can use its native notification system via OSC 9 escape sequences:
{
"notificationSystem": "ghostty"
}
This sends notifications directly through the terminal instead of using system notification tools. Works on any platform where Ghostty is running.
macOS: Ghostty delivers notifications through macOS Notification Center, which plays its own default sound. This can result in duplicate audio with the plugin's sound effects. Set suppressGhosttySound to true to skip the plugin's sound:
{
"notificationSystem": "ghostty",
"suppressGhosttySound": true
}
Note: custom sounds configured via the sounds section still play — only default (bundled) sounds are suppressed.
If you're using Ghostty inside tmux, enable passthrough in your tmux config so OSC 9 notifications can pass through:
set -g allow-passthrough on
Then reload tmux config:
tmux source-file ~/.tmux.conf
When suppressWhenFocused is true (the default), notifications and sounds are skipped if the terminal running OpenCode is the active/focused window. The idea is simple: if you're already looking at it, you don't need an alert.
To disable this and always get notified:
{
"suppressWhenFocused": false
}
You can suppress complete and subagent_complete notifications for short-lived sessions. Set minDuration to the number of seconds a session must exceed to trigger a done notification:
{
"minDuration": 10
}
With the above, if OpenCode finishes in under 10 seconds, no notification, sound, bell, or command is fired. Default is 0 (no threshold).
This is independent of command.minDuration, which only controls whether the custom command runs.
| Platform | Method | Requirements | Status |
|---|---|---|---|
| macOS | AppleScript (System Events) | None | Untested |
| Linux X11 | xdotool | xdotool installed | Untested |
| Linux Wayland (Hyprland) | hyprctl activewindow | None | Tested |
| Linux Wayland (Niri) | niri msg --json focused-window | None | Tested |
| Linux Wayland (Sway) | swaymsg -t get_tree | None | Untested |
| Linux Wayland (KDE) | kdotool | kdotool installed | Tested |
| Linux Wayland (GNOME) | Not supported | - | Falls back to always notifying |
| Linux Wayland (river, dwl, Cosmic, etc.) | Not supported | - | Falls back to always notifying |
| Windows | GetForegroundWindow() via PowerShell | None | Untested |
Unsupported compositors: Wayland has no standard protocol for querying the focused window. Each compositor has its own IPC, and GNOME intentionally doesn't expose focus information. Unsupported compositors fall back to always notifying.
tmux/screen: When running inside tmux, focus detection uses tmux pane state (session_attached, window_active, pane_active) via tmux display-message. This keeps suppression accurate when switching panes/windows/sessions. On Linux setups where window focus cannot be detected at all, tmux pane state is also used as a best-effort fallback. GNU Screen is not currently handled (falls back to always notifying).
WezTerm panes: When running in WezTerm with WEZTERM_PANE set, focus suppression is pane-aware via wezterm cli list-clients --format json. This means notifications are shown when you switch to a different WezTerm pane/tab.
Fail-open design: If detection fails for any reason (missing tools, unknown compositor, permissions), it falls back to always notifying. It never silently eats your notifications.
If you test on a platform marked "Untested" and it works (or doesn't), please open an issue and let us know.
By default, each notification appears as a separate entry. During active sessions this can create noise when multiple events fire quickly (e.g. permission + complete + question).
Enable grouping to replace notifications in-place instead of stacking:
{
"linux": {
"grouping": true
}
}
With grouping enabled, each new notification replaces the previous one so you only see the latest event. This requires notify-send 0.8+ (standard on Ubuntu 22.04+, Debian 12+, Fedora 36+, Arch). On older systems it falls back to the default stacking behavior automatically.
Works with all major notification daemons (GNOME, dunst, mako, swaync, etc.) on both X11 and Wayland.
On KDE Plasma/Wayland, clicking the popup body is not consistently delivered as a notification activation event.This plugin uses an explicit notification action button instead:
When clicked, the plugin runs its terminal-focus path. On KDE with kdotool installed, it auto-captures the startup terminal window ID and jumps back to that pinned window.
The action button is only enabled on Linux KDE sessions where kdotool is available.
OpenCode caches plugin packages under ~/.cache/opencode. If you switch between latest, beta, or a pinned version and OpenCode still uses the old plugin, close OpenCode and remove the cached package.
Linux/macOS:
rm -rf ~/.cache/opencode/packages/@mohak34/opencode-notifier*
rm -rf ~/.cache/opencode/node_modules/@mohak34/opencode-notifier
rm -f ~/.cache/opencode/bun.lock
Windows PowerShell:
Remove-Item -Recurse -Force "$env:USERPROFILE\.cache\opencode\packages\@mohak34\opencode-notifier*" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "$env:USERPROFILE\.cache\opencode\node_modules\@mohak34\opencode-notifier" -ErrorAction SilentlyContinue
Remove-Item -Force "$env:USERPROFILE\.cache\opencode\bun.lock" -ErrorAction SilentlyContinue
Then reopen OpenCode. It will download the plugin again.
To avoid cache confusion while testing, pin the exact version in opencode.json instead of using a moving tag:
{
"plugin": ["@mohak34/opencode-notifier@x.y.z"]
}
Check the version published under a tag:
npm view @mohak34/opencode-notifier@latest version
npm view @mohak34/opencode-notifier@beta version
Check the version OpenCode cached:
cat ~/.cache/opencode/packages/@mohak34/opencode-notifier@latest/node_modules/@mohak34/opencode-notifier/package.json | grep version
If you use @beta or a pinned version, replace latest in the path with beta or the exact version, for example 0.2.9-beta.0.
macOS: Not seeing notifications? Go to System Settings > Notifications > Script Editor, make sure it's set to Banners or Alerts.
macOS: node-notifier not showing notifications? Switch back to osascript. Some users report node-notifier works for sounds but not visual notifications on certain macOS versions.
Linux: No notifications? Install libnotify-bin:
sudo apt install libnotify-bin # Debian/Ubuntu
sudo dnf install libnotify # Fedora
sudo pacman -S libnotify # Arch
Test with: notify-send "Test" "Hello"
Linux: No sounds?
Install one of: paplay, aplay, mpv, or ffplay
KDE Plasma: jumps to wrong terminal window or doesn't jump?
Jump back feature tested on:
Most terminal emulators should work fine, but there can be exceptions.
Known limitations:
You can still override manually (if needed) by pinning an explicit window ID:
export OPENCODE_NOTIFIER_WINDOW_ID="$(kdotool getactivewindow)"
opencode
Manual pinning bypasses heuristic window matching and should activate that exact window on notification action click.
X11 deterministic jump-back
xdotool support is possible for the same startup pin behaviorWindows: Custom sounds not working?
C:/Users/YourName/sounds/alert.wav (not ~/)Windows WSL notifications not working? WSL doesn't have a native notification daemon. Use PowerShell commands instead:
{
"notification": false,
"sound": true,
"command": {
"enabled": true,
"path": "powershell.exe",
"args": [
"-Command",
"$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('{message}', 5, 'OpenCode - {event}', 0+64)"
]
}
}
Windows: OpenCode crashes when notifications appear? This is a known Bun issue on Windows. Disable native notifications and use PowerShell popups:
{
"notification": false,
"sound": true,
"command": {
"enabled": true,
"path": "powershell.exe",
"args": [
"-Command",
"$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('{message}', 5, 'OpenCode - {event}', 0+64)"
]
}
}
Plugin not loading?
opencode.json or config.json syntaxPlugin installed but no notifications/sounds?
suppressWhenFocused: when true (default), notifications are skipped while OpenCode terminal is focused. Set to false to always notify.enableOnDesktop: defaults to false, so the plugin won't run on Desktop/Web clients. Set to true if you need it there.cat ~/.cache/opencode/packages/@mohak34/opencode-notifier@latest/node_modules/@mohak34/opencode-notifier/package.json | grep version
If you use @beta or a pinned version, replace latest in the path with beta or the exact version.See CHANGELOG.md
MIT
TypeScript
100.0%
OpenCode plugin for desktop notifications and sounds on permission, completion, and error events.
818
stars
205
commits
TypeScript
primary language
Jul 19, 2026
updated
OpenCode plugin that plays sounds and sends system notifications when permission is needed, generation completes, errors occur, or the question tool is invoked. Works on macOS, Linux, and Windows.
Add this to your opencode.json:
{
"plugin": ["@mohak34/opencode-notifier@latest"]
}
Restart OpenCode. Done.
You'll get notified when:
There's also subagent_complete for when subagents finish, and user_cancelled for when you press ESC to abort -- both are silent by default so you don't get spammed.
macOS: Nothing to do, works out of the box. Shows the Script Editor icon.
Linux: Should work if you already have a notification system setup. If not install libnotify:
sudo apt install libnotify-bin # Ubuntu/Debian
sudo dnf install libnotify # Fedora
sudo pacman -S libnotify # Arch
For sounds, you need one of: paplay, aplay, mpv, or ffplay
Windows: Works out of the box. But heads up:
.wav files work (not mp3)C:/Users/You/sounds/alert.wav not ~/WSL: It's recommeneded to set customIconPath pointing to a file on Windows filesystem
due to issues with path translation (can be copied from logos folder from this repository).
This path will be passed down to snoretoast-*.exe
In opencode-notifier.json config:
"showIcon": true,
"customIconPath": "C:\\Users\\jhon\\Documents\\opencode-logo-dark.png",
Create ~/.config/opencode/opencode-notifier.json with the defaults:
{
"sound": true,
"notification": true,
"bell": false,
"timeout": 5,
"showProjectName": true,
"showFullPath": false,
"showSessionTitle": false,
"showIcon": true,
"customIconPath": null,
"suppressWhenFocused": true,
"enableOnDesktop": false,
"notificationSystem": "osascript",
"suppressGhosttySound": false,
"linux": {
"grouping": false
},
"minDuration": 0,
"command": {
"enabled": false,
"path": "/path/to/command",
"args": ["--event", "{event}", "--message", "{message}"],
"minDuration": 0
},
"events": {
"permission": { "sound": true, "notification": true, "command": true, "bell": false },
"complete": { "sound": true, "notification": true, "command": true, "bell": false },
"subagent_complete": { "sound": false, "notification": false, "command": true, "bell": false },
"error": { "sound": true, "notification": true, "command": true, "bell": false },
"question": { "sound": true, "notification": true, "command": true, "bell": false },
"user_cancelled": { "sound": false, "notification": false, "command": true, "bell": false },
"plan_exit": { "sound": true, "notification": true, "command": true, "bell": false },
"session_started": { "sound": true, "notification": false, "command": true, "bell": false },
"user_message": { "sound": true, "notification": false, "command": true, "bell": false },
"client_connected": { "sound": true, "notification": false, "command": true, "bell": false }
},
"messages": {
"permission": "Session needs permission: {sessionTitle}",
"complete": "Session has finished: {sessionTitle}",
"subagent_complete": "Subagent task completed: {sessionTitle}",
"error": "Session encountered an error: {sessionTitle}",
"question": "Session has a question: {sessionTitle}",
"user_cancelled": "Session was cancelled by user: {sessionTitle}",
"plan_exit": "Plan ready for review: {sessionTitle}",
"session_started": "Session started: {sessionTitle}",
"user_message": "User sent a message: {sessionTitle}",
"client_connected": "OpenCode connected"
},
"sounds": {
"permission": null,
"complete": null,
"subagent_complete": null,
"error": null,
"question": null,
"user_cancelled": null,
"plan_exit": null,
"session_started": null,
"user_message": null,
"client_connected": null
},
"volumes": {
"permission": 1,
"complete": 1,
"subagent_complete": 1,
"error": 1,
"question": 1,
"user_cancelled": 1,
"plan_exit": 1,
"session_started": 1,
"user_message": 1,
"client_connected": 1
}
}
{
"sound": true,
"notification": true,
"bell": false,
"timeout": 5,
"showProjectName": true,
"showFullPath": false,
"showSessionTitle": false,
"showIcon": true,
"suppressWhenFocused": true,
"enableOnDesktop": false,
"notificationSystem": "osascript",
"suppressGhosttySound": false
}
sound - Turn sounds on/off (default: true)notification - Turn notifications on/off (default: true)bell - Emit terminal BEL (\x07) on events (default: false). Behavior depends on your terminal/WM settingstimeout - How long notifications show in seconds, Linux only (default: 5)showProjectName - Show folder name in notification title (default: true)showFullPath - Show full absolute path instead of folder name in notification title and {projectName} token (default: false). When true, shows OpenCode (/home/user/projects/myapp) instead of OpenCode (myapp)showSessionTitle - Include the session title in notification messages via {sessionTitle} placeholder (default: false)showIcon - Show OpenCode icon, Windows/Linux only (default: true)customIconPath - Path to a custom icon for notifications. Useful on WSL where Windows paths are needed (default: null)suppressWhenFocused - Skip notifications and sounds when the terminal is the active window (default: true). See Focus detection for platform detailsenableOnDesktop - Run the plugin on Desktop and Web clients (default: false). When false, the plugin only runs on CLI. Set to true if you want notifications/sounds/commands on Desktop/Web — useful if you want custom commands (Telegram, webhooks) but don't care about built-in notificationsnotificationSystem - macOS only: "osascript", "node-notifier", or "ghostty" (default: "osascript"). Use "ghostty" if you're running Ghostty terminal for native OSC 9 notificationssuppressGhosttySound - macOS only: when true with notificationSystem: "ghostty", skips the plugin's sound to avoid duplicating macOS Notification Center's default sound (default: false)minDuration - Suppress complete and subagent_complete notifications when session finishes faster than this many seconds (default: 0). See Minimum duration thresholdlinux.grouping - Linux only: replace notifications in-place instead of stacking (default: false). Requires notify-send 0.8+Control each event separately:
{
"events": {
"permission": { "sound": true, "notification": true, "command": true, "bell": false },
"complete": { "sound": true, "notification": true, "command": true, "bell": false },
"subagent_complete": { "sound": false, "notification": false, "command": true, "bell": false },
"error": { "sound": true, "notification": true, "command": true, "bell": false },
"question": { "sound": true, "notification": true, "command": true, "bell": false },
"user_cancelled": { "sound": false, "notification": false, "command": true, "bell": false },
"plan_exit": { "sound": true, "notification": true, "command": true, "bell": false },
"session_started": { "sound": true, "notification": false, "command": true, "bell": false },
"user_message": { "sound": true, "notification": false, "command": true, "bell": false },
"client_connected": { "sound": true, "notification": false, "command": true, "bell": false }
}
}
user_cancelled fires when you press ESC to abort a session. It's silent by default so intentional cancellations don't trigger error alerts. Set sound or notification to true if you want confirmation when cancelling.
session_started fires when a new top-level session is created. user_message fires when a user message is submitted in a top-level session. client_connected fires shortly after the plugin initializes and is best-effort (there is no dedicated SDK connection event from plugin context).
The command property controls whether the custom command (see Custom commands) runs for that event. Defaults to true for all events. Set it to false to suppress the command for specific events without disabling it globally.
bell is terminal-driven and may be audible, visual, both, or ignored depending on your terminal setup. Quick check: printf '\a'.
Or use true/false for both:
{
"events": {
"complete": false
}
}
Customize the notification text:
{
"messages": {
"permission": "Session needs permission: {sessionTitle}",
"complete": "Session has finished: {sessionTitle}",
"subagent_complete": "Subagent task completed: {sessionTitle}",
"error": "Session encountered an error: {sessionTitle}",
"question": "Session has a question: {sessionTitle}",
"user_cancelled": "Session was cancelled by user: {sessionTitle}",
"plan_exit": "Plan ready for review: {sessionTitle}",
"session_started": "Session started: {sessionTitle}",
"user_message": "User sent a message: {sessionTitle}",
"client_connected": "OpenCode connected"
}
}
Messages support placeholder tokens that get replaced with actual values:
{sessionTitle} - The title/summary of the current session (e.g. "Fix login bug"){agentName} - Subagent name extracted from session titles with (@name subagent) suffix (e.g. builder, codebase-researcher), empty for non-subagent sessions{projectName} - The project folder name{timestamp} - Current time in HH:MM:SS format (e.g. "14:30:05"){turn} - Global notification counter that persists across restarts (e.g. 1, 2, 3). Stored in ~/.config/opencode/opencode-notifier-state.jsonWhen showSessionTitle is false, {sessionTitle} is replaced with an empty string. Any trailing separators (: , -, |) are automatically cleaned up when a placeholder resolves to empty.
To disable session titles in messages without changing showSessionTitle, just remove the {sessionTitle} placeholder from your custom messages.
The {timestamp} and {turn} placeholders also work in custom command args.
Use your own sound files:
{
"sounds": {
"permission": "/path/to/alert.wav",
"complete": "/path/to/done.wav",
"subagent_complete": "/path/to/subagent-done.wav",
"error": "/path/to/error.wav",
"question": "/path/to/question.wav",
"user_cancelled": "/path/to/cancelled.wav",
"plan_exit": "/path/to/plan-ready.wav",
"session_started": "/path/to/session-started.wav",
"user_message": "/path/to/user-message.wav",
"client_connected": "/path/to/client-connected.wav"
}
}
Platform notes:
Set per-event volume from 0 to 1:
{
"volumes": {
"permission": 0.6,
"complete": 0.3,
"subagent_complete": 0.15,
"error": 1,
"question": 0.7,
"user_cancelled": 0.5,
"plan_exit": 0.6,
"session_started": 0.35,
"user_message": 0.2,
"client_connected": 0.45
}
}
0 = mute, 1 = full volume0..1 are clamped automaticallyRun your own script when something happens. Use {event}, {message}, {sessionTitle}, {agentName}, {projectName}, {timestamp}, and {turn} as placeholders:
{
"command": {
"enabled": true,
"path": "/path/to/your/script",
"args": ["{event}", "{message}"],
"minDuration": 10
}
}
enabled - Turn command on/offpath - Path to your script/executableargs - Arguments to pass, can use {event}, {message}, {sessionTitle}, {agentName}, {projectName}, {timestamp}, and {turn} tokensminDuration - Skip if response was quick, avoids spam (seconds){
"command": {
"enabled": true,
"path": "/bin/bash",
"args": [
"-c",
"echo '[{event}] {message}' >> /tmp/opencode.log"
]
}
}
osascript (default): Reliable but shows Script Editor icon
{
"notificationSystem": "osascript"
}
node-notifier: Shows OpenCode icon but might miss notifications sometimes
{
"notificationSystem": "node-notifier"
}
NOTE: If you go with node-notifier and start missing notifications, just switch back or remove the option from the config. Users have reported issues with using node-notifier for receiving only sounds and no notification popups.
If you're using Ghostty terminal, you can use its native notification system via OSC 9 escape sequences:
{
"notificationSystem": "ghostty"
}
This sends notifications directly through the terminal instead of using system notification tools. Works on any platform where Ghostty is running.
macOS: Ghostty delivers notifications through macOS Notification Center, which plays its own default sound. This can result in duplicate audio with the plugin's sound effects. Set suppressGhosttySound to true to skip the plugin's sound:
{
"notificationSystem": "ghostty",
"suppressGhosttySound": true
}
Note: custom sounds configured via the sounds section still play — only default (bundled) sounds are suppressed.
If you're using Ghostty inside tmux, enable passthrough in your tmux config so OSC 9 notifications can pass through:
set -g allow-passthrough on
Then reload tmux config:
tmux source-file ~/.tmux.conf
When suppressWhenFocused is true (the default), notifications and sounds are skipped if the terminal running OpenCode is the active/focused window. The idea is simple: if you're already looking at it, you don't need an alert.
To disable this and always get notified:
{
"suppressWhenFocused": false
}
You can suppress complete and subagent_complete notifications for short-lived sessions. Set minDuration to the number of seconds a session must exceed to trigger a done notification:
{
"minDuration": 10
}
With the above, if OpenCode finishes in under 10 seconds, no notification, sound, bell, or command is fired. Default is 0 (no threshold).
This is independent of command.minDuration, which only controls whether the custom command runs.
| Platform | Method | Requirements | Status |
|---|---|---|---|
| macOS | AppleScript (System Events) | None | Untested |
| Linux X11 | xdotool | xdotool installed | Untested |
| Linux Wayland (Hyprland) | hyprctl activewindow | None | Tested |
| Linux Wayland (Niri) | niri msg --json focused-window | None | Tested |
| Linux Wayland (Sway) | swaymsg -t get_tree | None | Untested |
| Linux Wayland (KDE) | kdotool | kdotool installed | Tested |
| Linux Wayland (GNOME) | Not supported | - | Falls back to always notifying |
| Linux Wayland (river, dwl, Cosmic, etc.) | Not supported | - | Falls back to always notifying |
| Windows | GetForegroundWindow() via PowerShell | None | Untested |
Unsupported compositors: Wayland has no standard protocol for querying the focused window. Each compositor has its own IPC, and GNOME intentionally doesn't expose focus information. Unsupported compositors fall back to always notifying.
tmux/screen: When running inside tmux, focus detection uses tmux pane state (session_attached, window_active, pane_active) via tmux display-message. This keeps suppression accurate when switching panes/windows/sessions. On Linux setups where window focus cannot be detected at all, tmux pane state is also used as a best-effort fallback. GNU Screen is not currently handled (falls back to always notifying).
WezTerm panes: When running in WezTerm with WEZTERM_PANE set, focus suppression is pane-aware via wezterm cli list-clients --format json. This means notifications are shown when you switch to a different WezTerm pane/tab.
Fail-open design: If detection fails for any reason (missing tools, unknown compositor, permissions), it falls back to always notifying. It never silently eats your notifications.
If you test on a platform marked "Untested" and it works (or doesn't), please open an issue and let us know.
By default, each notification appears as a separate entry. During active sessions this can create noise when multiple events fire quickly (e.g. permission + complete + question).
Enable grouping to replace notifications in-place instead of stacking:
{
"linux": {
"grouping": true
}
}
With grouping enabled, each new notification replaces the previous one so you only see the latest event. This requires notify-send 0.8+ (standard on Ubuntu 22.04+, Debian 12+, Fedora 36+, Arch). On older systems it falls back to the default stacking behavior automatically.
Works with all major notification daemons (GNOME, dunst, mako, swaync, etc.) on both X11 and Wayland.
On KDE Plasma/Wayland, clicking the popup body is not consistently delivered as a notification activation event.This plugin uses an explicit notification action button instead:
When clicked, the plugin runs its terminal-focus path. On KDE with kdotool installed, it auto-captures the startup terminal window ID and jumps back to that pinned window.
The action button is only enabled on Linux KDE sessions where kdotool is available.
OpenCode caches plugin packages under ~/.cache/opencode. If you switch between latest, beta, or a pinned version and OpenCode still uses the old plugin, close OpenCode and remove the cached package.
Linux/macOS:
rm -rf ~/.cache/opencode/packages/@mohak34/opencode-notifier*
rm -rf ~/.cache/opencode/node_modules/@mohak34/opencode-notifier
rm -f ~/.cache/opencode/bun.lock
Windows PowerShell:
Remove-Item -Recurse -Force "$env:USERPROFILE\.cache\opencode\packages\@mohak34\opencode-notifier*" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force "$env:USERPROFILE\.cache\opencode\node_modules\@mohak34\opencode-notifier" -ErrorAction SilentlyContinue
Remove-Item -Force "$env:USERPROFILE\.cache\opencode\bun.lock" -ErrorAction SilentlyContinue
Then reopen OpenCode. It will download the plugin again.
To avoid cache confusion while testing, pin the exact version in opencode.json instead of using a moving tag:
{
"plugin": ["@mohak34/opencode-notifier@x.y.z"]
}
Check the version published under a tag:
npm view @mohak34/opencode-notifier@latest version
npm view @mohak34/opencode-notifier@beta version
Check the version OpenCode cached:
cat ~/.cache/opencode/packages/@mohak34/opencode-notifier@latest/node_modules/@mohak34/opencode-notifier/package.json | grep version
If you use @beta or a pinned version, replace latest in the path with beta or the exact version, for example 0.2.9-beta.0.
macOS: Not seeing notifications? Go to System Settings > Notifications > Script Editor, make sure it's set to Banners or Alerts.
macOS: node-notifier not showing notifications? Switch back to osascript. Some users report node-notifier works for sounds but not visual notifications on certain macOS versions.
Linux: No notifications? Install libnotify-bin:
sudo apt install libnotify-bin # Debian/Ubuntu
sudo dnf install libnotify # Fedora
sudo pacman -S libnotify # Arch
Test with: notify-send "Test" "Hello"
Linux: No sounds?
Install one of: paplay, aplay, mpv, or ffplay
KDE Plasma: jumps to wrong terminal window or doesn't jump?
Jump back feature tested on:
Most terminal emulators should work fine, but there can be exceptions.
Known limitations:
You can still override manually (if needed) by pinning an explicit window ID:
export OPENCODE_NOTIFIER_WINDOW_ID="$(kdotool getactivewindow)"
opencode
Manual pinning bypasses heuristic window matching and should activate that exact window on notification action click.
X11 deterministic jump-back
xdotool support is possible for the same startup pin behaviorWindows: Custom sounds not working?
C:/Users/YourName/sounds/alert.wav (not ~/)Windows WSL notifications not working? WSL doesn't have a native notification daemon. Use PowerShell commands instead:
{
"notification": false,
"sound": true,
"command": {
"enabled": true,
"path": "powershell.exe",
"args": [
"-Command",
"$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('{message}', 5, 'OpenCode - {event}', 0+64)"
]
}
}
Windows: OpenCode crashes when notifications appear? This is a known Bun issue on Windows. Disable native notifications and use PowerShell popups:
{
"notification": false,
"sound": true,
"command": {
"enabled": true,
"path": "powershell.exe",
"args": [
"-Command",
"$wshell = New-Object -ComObject Wscript.Shell; $wshell.Popup('{message}', 5, 'OpenCode - {event}', 0+64)"
]
}
}
Plugin not loading?
opencode.json or config.json syntaxPlugin installed but no notifications/sounds?
suppressWhenFocused: when true (default), notifications are skipped while OpenCode terminal is focused. Set to false to always notify.enableOnDesktop: defaults to false, so the plugin won't run on Desktop/Web clients. Set to true if you need it there.cat ~/.cache/opencode/packages/@mohak34/opencode-notifier@latest/node_modules/@mohak34/opencode-notifier/package.json | grep version
If you use @beta or a pinned version, replace latest in the path with beta or the exact version.See CHANGELOG.md
MIT
TypeScript
100.0%