2 Notifier
OpenCode v2 plugin that sends Slack and Discord notifications when sessions complete, error, need permission, or ask a question.
0
508
近 7 天 508
36.6
生态多维模型
7 天前
2026-09-27
快速安装与配置
opencode.json写入当前项目的 opencode.json,只对这个仓库生效。
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode2-notifier@0.1.2"]
}写入 ~/.config/opencode/opencode.json,对所有项目生效。
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode2-notifier@0.1.2"]
}若你要在本地改造这个插件,先装到项目里再从本地路径引用。
shell
pnpm add -D opencode2-notifierOpenCode 启动时会通过内嵌运行时自动加载 npm 依赖并缓存至本地目录,无需手动在全局环境执行安装。
OpenCode v2 plugin that notifies you on Slack and/or Discord when an agent session finishes, crashes, waits on a permission, or asks you a question. Built for the "OpenCode is working on my laptop while I'm elsewhere" workflow.
- Plugin ID:
opencode2-notifier - Runtime: OpenCode v2 (
@opencode/pluginPromise API) - Sinks: Slack incoming webhook, Discord channel webhook (use one or both)
Contents
- How it works
- Install
- Verifying installation
- Slack webhook setup
- Discord webhook setup
- Configuration reference
- Event mapping
- Behavior details
- Message format
- Local development
- Releasing
- Troubleshooting
- Security notes
- Changelog
- License
How it works
On setup, the plugin:
- Reads and validates
ctx.options(webhook URLs, event allowlist, debounce). - Subscribes to the live server event stream via
ctx.event.subscribe(). - Classifies each event (
src/events.ts) intocomplete | error | permission | question. - Enriches it with session title + elapsed time via
ctx.session.get(). - Fans out to every configured sink with
Promise.allSettled— one sink failing never blocks the other. - Cleans up timers and aborts the stream on plugin unload.
If no webhook is configured, the plugin logs a warning and disables itself instead of breaking OpenCode.
Install
Option A — from npm (recommended)
opencode plugin add opencode2-notifier
plugin addregisters the plugin as a bare string with no options, and this plugin requires at least one webhook URL to do anything. Convert the entry to the object form below, otherwise it disables itself with a[opencode2-notifier] disabled: ... missing webhookwarning.
Then add options to your opencode.jsonc (global ~/.config/opencode/opencode.jsonc or project .opencode/opencode.jsonc):
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
{
"package": "opencode2-notifier",
"options": {
"slackWebhookUrl": "{env:SLACK_WEBHOOK_URL}",
"discordWebhookUrl": "{env:DISCORD_WEBHOOK_URL}",
"events": ["complete", "error", "permission", "question"],
"debounceMs": 10000,
"notifyChildSessions": false
}
}
]
}
Option B — local checkout
git clone https://github.com/asliutkarsh/opencode2-notifier.git
Point OpenCode at it:
{
"plugins": [{ "package": "/absolute/path/opencode2-notifier", "options": { "discordWebhookUrl": "{env:DISCORD_WEBHOOK_URL}" } }]
}
Restart the service after config changes:
opencode service restart
The env var must be visible to the background service process, not just your current shell. On Windows, set it as a User environment variable and then restart the service:
[Environment]::SetEnvironmentVariable('DISCORD_WEBHOOK_URL', '<url>', 'User') opencode service restart
Verifying installation
This plugin is event-only: it registers no tools, commands, agents, or skills, so you will not see it in tool lists, /commands, or the TUI. That is expected. Verify instead via:
opencode plugin list— should showopencode2-notifier 0.1.2(or newer).- Server log (
~/.local/share/opencode/log/opencode.log) — should containloading plugin id=opencode2-notifierwith no following error. - Trigger an event (e.g. run a task that needs permission) and check your Slack/Discord channel.
Slack webhook setup
- Open your Slack workspace → Settings & administration → Manage apps → Custom Integrations → Incoming WebHooks (or use Workflow Builder webhooks).
- Add a webhook for the channel you want (e.g.
#agent-alerts), copy thehttps://hooks.slack.com/services/...URL. - Export it where OpenCode runs:
The plugin also acceptsexport SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."{env:ANY_VAR_NAME}inslackWebhookUrl, or theOPCODE2_NOTIFIER_SLACK_WEBHOOK_URLfallback.
Discord webhook setup
- In Discord: channel Edit Channel → Integrations → Webhooks → New Webhook, pick a name/avatar, copy the
https://discord.com/api/webhooks/...URL. - Export it where OpenCode runs:
The plugin also acceptsexport DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."{env:ANY_VAR_NAME}indiscordWebhookUrl, or theOPCODE2_NOTIFIER_DISCORD_WEBHOOK_URLfallback.
Configuration reference
| Key | Type | Default | Description |
|---|---|---|---|
slackWebhookUrl |
string (URL or {env:VAR}) |
SLACK_WEBHOOK_URL → OPCODE2_NOTIFIER_SLACK_WEBHOOK_URL |
Slack sink. Optional if Discord is set. |
discordWebhookUrl |
string (URL or {env:VAR}) |
DISCORD_WEBHOOK_URL → OPCODE2_NOTIFIER_DISCORD_WEBHOOK_URL |
Discord sink. Optional if Slack is set. |
events |
("complete" | "error" | "permission" | "question")[] |
all four | Allowlist of notification kinds. Unknown values are ignored. |
debounceMs |
number (ms) |
10000 |
Delay-and-replace window for complete only. |
notifyChildSessions |
boolean |
false |
When false, sessions with a parentID (subagents) are skipped. |
At least one of slackWebhookUrl / discordWebhookUrl must resolve to an http(s) URL or setup throws and the plugin disables itself with a warning.
Minimal Discord-only example:
{
"plugins": [{ "package": "opencode2-notifier", "options": { "discordWebhookUrl": "{env:DISCORD_WEBHOOK_URL}" } }]
}
Event mapping
Based on the V2 V2Event union (@opencode/client):
V2 event.type |
Notifier kind | Delivery |
|---|---|---|
session.idle, session.execution.succeeded |
complete |
debounced per session (debounceMs) |
session.execution.failed, session.step.failed, session.tool.failed, session.compaction.failed |
error |
immediate (includes type: message) |
permission.asked |
permission |
immediate (includes action + first resources + message) |
form.created |
question |
immediate (includes form title) |
session.created |
— (internal) | records session start time for elapsed reporting + registers subagent children (parentID) |
Behavior details
- Debounce (
complete): rapid duplicate idle/success events for the same session collapse into one post afterdebounceMs. Timers areunref'd so they never keep the process alive. - Dedupe (all kinds): 30s per
kind + sessionIDwindow suppresses exact duplicates (e.g. permission re-asked in a tight loop). - Child sessions:
ctx.session.get()exposesparentID; whennotifyChildSessionsisfalsethose are dropped silently. - Enrichment failures: if
session.getfails (session gone, transient error), the notification still sends with directory only. - Fan-out: sinks send concurrently; a Slack 5xx does not cancel the Discord post and vice versa. Failures are
console.warn'd, never thrown. - Location scoping: global plugins load once per location. Each instance only notifies for sessions in its own directory (compared case-insensitively via
session.location.directory), so one session yields exactly one post instead of one per location. - Subagent tracking (
src/subagents.ts): children are recorded fromsession.created(viaparentID) and marked finished onsession.execution.*/session.idle. Notifications carry aSubagents:line like2 running (Explore backend, Research docs) · 1 done. A child still marked running has not finished. Children that started before plugin load are unknown and omitted. Tracker is bounded (200 parents max). - Abort:
AbortSignalis threaded through the event iterator and bothfetchcalls; unload aborts everything and clears pending debounce timers.
Message format
Both sinks head every message with just the session name plus emoji (e.g. ✅ Curating plugins list), falling back to the kind title when untitled or blank. Remaining lines: short session ID, working directory, elapsed time, subagent status, and kind-specific detail.
Slack (src/slack.ts): text fallback + heading section, plus a details section whenever ID/dir/elapsed/subagents/detail lines exist.
Discord (src/discord.ts): content ping line + rich embed with per-kind color (complete green 0x57F287, error red 0xED4245, permission yellow 0xFEE75C, question blurple 0x5865F2), fields, and timestamp. Long details are truncated to Discord's 2000-char limits.
Local development
bun install
bun test # 28 tests: config, event classification, slack/discord payloads, location scoping, subagent tracker
bunx tsc --noEmit
Project layout:
src/
index.ts # Plugin.define, event loop, debounce/dedupe, fan-out
config.ts # option parsing + {env:VAR} resolution
events.ts # V2Event -> NotifyKind classification
slack.ts # Slack Block Kit payload + sender
discord.ts # Discord embed payload + sender
subagents.ts # subagent child tracking per parent session
*.test.ts # bun tests
Releasing
Releases are tag-driven: pushing a GitHub Release publishes that version to npm via .github/workflows/publish.yml (tests + typecheck + tag/version match check run first). The release-notes changelog is auto-generated from PR labels (see .github/release.yml).
One-time setup: create an npm granular access token (package scope opencode2-notifier, bypass 2FA enabled) and save it as the repo secret NPM_TOKEN (Repo → Settings → Secrets → Actions).
# 1. Bump version
npm version patch # or minor/major
git push origin main --tags
# 2. Cut the release (generates the changelog)
gh release create v$(bun -p "require('./package.json').version") --generate-notes
# 3. Publishing to npm happens automatically on release
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
[opencode2-notifier] disabled: ... missing webhook |
No URL resolved (common right after plugin add, which writes a bare string entry with no options) |
Convert to object form with slackWebhookUrl/discordWebhookUrl (see Install); export the env var; restart service |
| Plugin installed but "nothing to see" | Expected: event-only plugin registers no tools/commands/agents | See Verifying installation |
No complete post |
Still inside debounceMs, or duplicates deduped |
Lower debounceMs for testing (e.g. 2000); check service logs |
Slack webhook failed: 404 |
Revoked/rotated webhook | Recreate the incoming webhook, update env |
Discord webhook failed: 404 |
Deleted webhook or wrong path | Recreate via channel Integrations, update env |
| Child-session spam / silence | notifyChildSessions mismatch |
Set true to include subagents, false (default) for mains only |
| Events missing on old server | Server older than tested API | Use OpenCode v2.0.16+ / @opencode/plugin 2.0.18 |
Logs live in ~/.local/share/opencode/log/opencode.log — filter role=server for plugin lines.
Security notes
- Webhook URLs are secrets: never commit them. Use
{env:VAR}indirection and keep them in shell env or a secrets manager. - If a webhook URL leaks (chat logs, screenshots), regenerate it on the Slack/Discord side — old URLs keep working until revoked.
- Notifications include session titles, directory paths, and error text. Avoid pointing them at public channels if you work on sensitive repos.
Changelog
0.1.2
- Headings are just the session name + emoji (no more "OpenCode session complete —" prefix).
- New
Subagents:line with live running/done/failed counts and running titles.
0.1.1
- Fix duplicate posts: notify only for sessions in the plugin instance's own location (one post per session instead of one per loaded location). Show the session's directory in the message.
0.1.0
- Initial release: Slack + Discord sinks,
complete/error/permission/questionkinds, debounce + dedupe, child-session filter,{env:VAR}config.
License
MIT — see LICENSE.
同类生态推荐
Smart Notify
opencode-smart-notify
Desktop notifications for OpenCode that stay quiet when auto-approve handles the request
Notification
opencode-notification
OpenCode plugin for simple system notifications when permissions are needed, generation completes, or errors occur
Disunday
disunday
Discord bot for controlling OpenCode coding sessions