Profile
Back to NewsBack
GitHub Trending 24 min
Reader Mode
mohak34/opencode-notifier: OpenCode plugin for desktop notifications and sounds on permission, completion, and error events.

mohak34/opencode-notifier: OpenCode plugin for desktop notifications and sounds on permission, completion, and error events.

13 hours ago

opencode-notifier

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.

Quick Start

For OpenCode 1, add the package to opencode.json:

{
  "plugin": ["@mohak34/opencode-notifier@latest"]
}

For OpenCode 2, use plugins:

{
  "plugins": ["@mohak34/opencode-notifier@latest"]
}

Restart OpenCode. The package contains both implementations; you do not select a version manually. V2 loads the package's terminal component automatically in the interactive CLI.

OpenCode 2 delivery

Sounds, desktop popups, terminal bells, Ghostty notifications, and focus detection run in the terminal client. Custom commands run once per event on the server, including when no terminal is open. This uses the existing command configuration; no external notification service is bundled.

With a remote server, put sound and popup settings on your local computer and custom-command settings on the server. Event-command script paths refer to the server's filesystem. Click-command paths refer to the computer displaying the notification. Focus suppression applies to local alerts, not server commands. Each attached terminal receives alerts for its project.

opencode run and Desktop/Web clients do not load this terminal component, so they receive no plugin sound, popup, or bell. Server commands still run. V1 delivery and its enableOnDesktop behavior are unchanged.

V2 already includes a notification plugin. To avoid duplicate built-in alerts, add "-opencode.notifications" to the existing plugins list in ~/.config/opencode/cli.json:

{
  "plugins": ["*", "-opencode.notifications"]
}

Keep any other entries in that list. plan_exit remains a V1 event; V2 has no equivalent plan-ready signal, so that setting is inactive there. client_connected is best-effort: terminal startup for local alerts, server plugin startup for custom commands.

What it does

You'll get notified when:

  • OpenCode needs permission to run something
  • Your session finishes
  • An error happens
  • The question tool pops up
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.

Setup by platform

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:

  • Only .wav files work (not mp3)
  • Use full paths like 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",

Config file

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,
  "focusOnClick": 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
  }
}

All options

Global options

{
  "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 settings
  • timeout - 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 details
  • enableOnDesktop - V1 only: run the plugin on Desktop and Web clients (default: false). V2 runs commands on the server and local alerts in its terminal component; this flag does not control V2 delivery.
  • notificationSystem - macOS only: "osascript", "node-notifier", or "ghostty" (default: "osascript"). Use "ghostty" if you're running Ghostty terminal for native OSC 9 notifications
  • suppressGhosttySound - 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 threshold
  • linux.grouping - Linux only: replace notifications in-place instead of stacking (default: false). Requires notify-send 0.8+

Events

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 is best-effort. On V1 it fires shortly after plugin initialization. On V2, terminal initialization triggers local alerts and server plugin initialization triggers the custom command; it does not track every client reconnection.

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
  }
}

Messages

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.json
When 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.

Sounds

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:

  • macOS/Linux: .wav or .mp3 files work
  • Windows: Only .wav files work
  • If file doesn't exist, falls back to bundled sound

Volumes

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 volume
  • Values outside 0..1 are clamped automatically
  • On Windows, playback still works but custom volume may not be honored by the default player

Custom commands

command runs a script when an event occurs. On V2 this runs on the server, even without a terminal attached. Use {event}, {message}, {sessionTitle}, {sessionID}, {agentName}, {projectName}, {timestamp}, and {turn} as placeholders:

{
  "command": {
    "enabled": true,
    "path": "/path/to/your/script",
    "args": ["{event}", "{message}"],
    "minDuration": 10
  }
}
  • enabled - Turn command on/off
  • path - Path to your script/executable
  • args - Arguments to pass, can use {event}, {message}, {sessionTitle}, {sessionID}, {agentName}, {projectName}, {timestamp}, and {turn} tokens
  • minDuration - Skip if response was quick, avoids spam (seconds)
{sessionID} is the ID of the session that triggered the event (e.g. ses_0048b8aa...), so a script can tell concurrent sessions in the same project apart. It is empty for events without a session, such as client_connected.

Token values are passed as argv values and are not shell-escaped for use inside script source. Do not put {message}, {sessionTitle}, or other dynamic tokens inside a sh -c, bash -c, powershell -Command, or similar script string. Use a wrapper script and pass the tokens as separate arguments instead. Custom commands run with the same user permissions as OpenCode, so only enable scripts you trust.

Run a command when clicking a notification

onClickCommand runs locally when you activate a notification, separately from the event-time command. It is disabled by default. It accepts enabled, path, and args with the same placeholders. The arguments keep the originating notification's session context, even if you switch sessions before clicking.

{
  "focusOnClick": false,
  "onClickCommand": {
    "enabled": true,
    "path": "/path/to/focus-opencode",
    "args": ["{sessionID}", "{projectName}"]
  }
}

On Linux, use the explicit Run command action button; popup body clicks are not reliably delivered. The notification daemon must support actions and notify-send must support --action. The click listener expires after timeout plus one second. Windows toasts and macOS node-notifier deliver their activation callback; AppleScript and Ghostty OSC notifications do not support this command.

focusOnClick defaults to true and enables the built-in KDE/GNOME jump-back when available. Set it to false for a script-only action. When both are enabled, clicking runs your script and focuses the terminal. On V2, configure click commands in the local terminal's configuration and event commands on the server. events..command controls event commands only; a click command is available for any enabled popup.

Example: Log events to a file

{
  "command": {
    "enabled": true,
    "path": "/bin/bash",
    "args": [
      "-c",
      "printf '[%s] %s\\n' \"$1\" \"$2\" >> /tmp/opencode.log",
      "opencode-notifier",
      "{event}",
      "{message}"
    ]
  }
}

macOS: Pick your notification style

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.

Ghostty notifications

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

Focus detection

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
}

Minimum duration threshold

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.

Completion with child sessions

Set "deferCompleteUntilChildrenIdle": true to wait for known child sessions before sending the parent's complete notification, sound, bell, or command. The default is false.

The plugin tracks native OpenCode child sessions and their descendants from creation and execution events. It sends one parent completion after all tracked child work finishes, fails, is interrupted, or is deleted. A new parent run cancels the pending completion. Work from third-party delegation plugins without native child-session events, or work already running before the notifier loads, cannot be tracked reliably.

deferredCompleteTimeout limits the wait in milliseconds, default 900000 (15 minutes). Expired pending alerts are dropped rather than reporting completion while work is still running.

Platform support

| 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) | AT-SPI (gdbus on the org.a11y.Bus) | gdbus installed | Tested (Ubuntu 26.04.1 LTS + GNOME Shell 50.1 + Ghostty 1.3.0) | | Linux Wayland (river, dwl, Cosmic, etc.) | Not supported | - | Falls back to always notifying | | Windows | GetForegroundWindow() via PowerShell | None | Untested |

GNOME Wayland: GNOME exposes no compositor API for the focused window (Introspect.GetWindows and Eval are access-denied) and XWayland tools like xdotool cannot see native Wayland windows, so focus is read from the accessibility bus instead: the active terminal window is the one whose AT-SPI ACTIVE state bit is set. Ghostty is matched by its /com/mitchellh/ghostty AT-SPI path, other terminals by app name (including the gnome-terminal-server AT-SPI alias). Window identity is bus@path since AT-SPI paths repeat across processes. Implemented and verified on Ubuntu 26.04.1 LTS + GNOME Shell 50.1 + Ghostty 1.3.0. With several terminal windows open, suppression compares against the window that was active at startup. Set OPENCODE_NOTIFIER_DEBUG=1 to log the focus backend decision.

Unsupported compositors: Wayland has no standard protocol for querying the focused window. Each compositor has its own IPC. Compositors without a backend (river, dwl, Cosmic, etc.) 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.

Zellij panes: With ZELLIJ_SESSION_NAME and ZELLIJ_PANE_ID set, suppression also checks zellij --session action list-clients. Switching away from the OpenCode pane or tab allows notifications even when the terminal window stays focused. With multiple clients, the pane counts as focused if any attached client focuses it. A missing tool, failed query, or detached session allows notifications. On Linux without window detection, pane focus is a best-effort fallback, as with tmux.

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.

Linux: Notification Grouping

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.

Linux: Jump back to terminal from notification

On KDE Plasma and GNOME Wayland, use the explicit Jump to terminal action button. Clicking the popup body is not reliably routed back to the plugin.

On KDE with kdotool installed, the plugin captures the startup terminal window ID and jumps back to that window.

On GNOME Wayland, the bundled OpenCode Notifier Jump Back Shell extension is needed only for the Jump to terminal action. Normal notification popups, sounds, and existing focus suppression work without it.

A GNOME Shell extension is a small add-on to the GNOME desktop. This one remembers the terminal window where OpenCode started. When you press Jump to terminal, it asks GNOME to bring that exact window and its workspace forward. The existing focus detection can identify Ghostty but cannot activate its window on GNOME Wayland.

Install the extension on the GNOME computer displaying the notifications. With a remote OpenCode server, this means your local desktop. Installing the OpenCode plugin does not automatically install this desktop extension.

From this repository or the installed npm package directory, copy the extension files:

mkdir -p ~/.local/share/gnome-shell/extensions/[email protected]
cp gnome-shell-extension/extension.js gnome-shell-extension/metadata.json ~/.local/share/gnome-shell/extensions/[email protected]/

Log out and back in so GNOME discovers the extension, then enable it:

gnome-extensions enable [email protected]

Restart OpenCode while the terminal window you want to return to is focused. Leave focusOnClick enabled, its default setting, then use the notification's Jump to terminal button.

The extension targets GNOME Shell 45 through 50. The button appears when the plugin successfully captured a startup window through the extension. Automated checks cover the extension logic and communication, but window switching still needs validation on a real GNOME desktop.

Notification delivery returns once notify-send prints the notification ID. The click listener lasts for the configured notification timeout plus a one-second grace period, then closes even if the notification daemon ignores expiry.

Updating

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/[email protected]"]
}

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.

Troubleshooting

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:

  • KWin with default floating windows
  • KWin + Krohnkite
  • Ghostty and Konsole
  • Warp
  • OpenCode in tmux inside VS Code terminal
Most terminal emulators should work fine, but there can be exceptions.

Known limitations:

  • Kitty is currently unsupported for this jump-back path (unstable focus targeting)
  • Yakuake sessions are not supported for activity-specific jump-back behavior
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 behavior
  • not implemented yet
Windows: Custom sounds not working?
  • Must be .wav format (not .mp3)
  • Use full Windows paths: C:/Users/YourName/sounds/alert.wav (not ~/)
  • Make sure the file actually plays in Windows Media Player
  • If using WSL, the path should be accessible from Windows
Windows WSL notifications not working? WSL doesn't have a native notification daemon. Use PowerShell commands instead:

Save this wrapper as C:\Users\YourName\bin\opencode-notifier-popup.ps1:

param(
  [string]$Message,
  [string]$Event
)

$wshell = New-Object -ComObject Wscript.Shell $wshell.Popup($Message, 5, ("OpenCode - {0}" -f $Event), 0+64)

{
  "notification": false,
  "sound": true,
  "command": {
    "enabled": true,
    "path": "powershell.exe",
    "args": [
      "-NoProfile",
      "-File",
      "C:\\Users\\YourName\\bin\\opencode-notifier-popup.ps1",
      "{message}",
      "{event}"
    ]
  }
}

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": [
      "-NoProfile",
      "-File",
      "C:\\Users\\YourName\\bin\\opencode-notifier-popup.ps1",
      "{message}",
      "{event}"
    ]
  }
}

Plugin not loading?

  • Check your opencode.json or config.json syntax
  • Clear the cache (see Updating section)
  • Restart OpenCode
Plugin installed but no notifications/sounds?
  • Check suppressWhenFocused: when true (default), notifications are skipped while OpenCode terminal is focused. Set to false to always notify.
  • On V1, check enableOnDesktop: it defaults to false. On V2, Desktop/Web and headless clients use server commands; local sounds and popups require the terminal component described above.
  • Verify the package 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.

TypeScript imports

Loading this plugin through OpenCode configuration does not require installing SDKs separately. If a TypeScript project imports the package directly, its declarations reference both OpenCode SDK generations. Install the optional type peers in that project:

bun add -d '@opencode-ai/plugin@^1.18.25' '@opencode/plugin@^2.0.18' '@opencode/client@^2.0.18'

These peers are optional to keep runtime-only installs lightweight. They are not automatically installed, so a V1-only TypeScript project importing the dual entrypoint also needs the V2 type peers. Adding peer metadata alone does not resolve missing type packages.

Changelog

See CHANGELOG.md

License

MIT

Chat with me