Never Miss an OpenCode Update: Introducing OpenCode Alerter
Project Overview
OpenCode Alerter is an OpenCode CLI plugin that sends push notifications whenever a command finishes, needs your input, or requests permissions.
Instead of staring at a terminal waiting for a long build, test run, or an interactive prompt, the plugin notifies your phone (or any device supported by Shoutrrr) the moment something important happens.
This keeps developers productive, reduces the risk of missed prompts, and makes unattended CI‑like workflows safer.
Key Features
- Command Completion Alerts – Get a notification as soon as a command ends, regardless of success or failure.
- Interactive Prompt Detection – Be warned when OpenCode asks for a password, confirmation, or any other input.
- Permission Request Alerts – Receive a notice whenever a command requests elevated permissions.
- Smart Filtering – Include or exclude commands with simple pattern rules, so you only hear about the tasks that matter.
- Configurable Thresholds – Set a minimum execution time (e.g., 3 minutes) to avoid noise from quick commands.
- Emoji Themes – Choose from minimal, standard, or rich emoji packs to personalize the notification style.
- Multiple Notification Services – Works with Pushover, Gotify, and any service supported by the Shoutrrr URL scheme.
Architecture Overview
A concise ASCII diagram shows how the plugin fits into the OpenCode workflow:
+-------------------+ +------------------------+ +-------------------+
| OpenCode CLI |→ | OpenCode Plugin System |→ | OpenCode Alerter |
| (user runs cmd) | | (hooks: execute, ask, | | (Tracker, Detector|
+-------------------+ | message) | | Formatter, Notifier)|
+-----------+------------+ +----------+--------+
| |
v v
+-------------------+ +-------------------+
| Notification | | Configuration |
| Service (Shoutrrr) | | (JSON file) |
+-------------------+ +-------------------+The plugin registers a handful of hooks, collects execution metadata, decides whether a notification is needed, formats a friendly message, and hands it off to Shoutrrr.
How It Works
1. Hook registration (internal)
When the plugin is loaded it registers four hooks:
// src/plugin.js
import {
handleCommandExecuteBefore,
handleCommandExecuteAfter,
handlePermissionAsk,
handleChatMessage,
} from './handlers.js';
export function registerHooks(opencode) {
opencode.hooks.on('command.execute.before', handleCommandExecuteBefore);
opencode.hooks.on('command.execute.after', handleCommandExecuteAfter);
opencode.hooks.on('permission.ask', handlePermissionAsk);
opencode.hooks.on('chat.message', handleChatMessage);
}2. Tracking command start
The command.execute.before hook stores the command string and start timestamp.
// src/tracker.js
const running = new Map(); // key: command ID, value: { cmd, start }
export function startCommand(id, cmd) {
running.set(id, { cmd, start: Date.now() });
}// src/handlers.js
import * as tracker from './tracker.js';
export function handleCommandExecuteBefore({ id, command }) {
tracker.startCommand(id, command);
}3. Determining whether to notify
When the command ends, the command.execute.after hook calculates the duration and asks the Detector if a notification is warranted.
// src/detector.js
import * as config from './config.js';
export function shouldNotify(command, durationMs) {
const threshold = config.get('thresholdMs', 0);
const filtered = config.get('filters', []);
const isFiltered = filtered.some(pat => command.includes(pat));
return durationMs > threshold && !isFiltered;
}// src/handlers.js
import * as tracker from './tracker.js';
import * as detector from './detector.js';
import * as formatter from './formatter.js';
import * as notifier from './notifier.js';
export async function handleCommandExecuteAfter({ id, success }) {
const info = tracker.finishCommand(id);
if (!info) return; // safety
const duration = Date.now() - info.start;
if (!detector.shouldNotify(info.cmd, duration)) return;
const message = formatter.formatMessage(info.cmd, duration, success);
await notifier.send(message);
}tracker.finishCommand simply removes the entry and returns the stored data:
// src/tracker.js (continued)
export function finishCommand(id) {
const data = running.get(id);
running.delete(id);
return data;
}4. Formatting the notification
The Formatter injects the selected emoji theme and builds a concise message.
// src/formatter.js
import * as config from './config.js';
export function formatMessage(command, durationMs, success) {
const theme = config.get('emojiTheme', 'standard');
const emojiMap = {
minimal: '',
standard: success ? '✅' : '❌',
rich: success ? '🚀' : '⚠️',
};
const emoji = emojiMap[theme] || '';
const secs = (durationMs / 1000).toFixed(1);
return `${emoji} ${success ? 'Success' : 'Failure'} – "${command}" took ${secs}s`;
}5. Sending the notification
The Notifier forwards the formatted text to every Shoutrrr URL defined in the configuration file.
// src/notifier.js
import * as config from './config.js';
import https from 'https';
import { URL } from 'url';
export async function send(message) {
const urls = config.get('shoutrrrUrls', []);
const promises = urls.map(urlStr => postToShoutrrr(urlStr, message));
await Promise.all(promises);
}
function postToShoutrrr(urlStr, message) {
const url = new URL(urlStr);
const payload = JSON.stringify({ message });
const options = {
method: 'POST',
hostname: url.hostname,
port: url.port || 443,
path: url.pathname + url.search,
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
};
return new Promise((resolve, reject) => {
const req = https.request(options, res => {
res.statusCode === 200 ? resolve() : reject(new Error(`HTTP ${res.statusCode}`));
});
req.on('error', reject);
req.write(payload);
req.end();
});
}6. Prompt & permission hooks
The chat.message and permission.ask hooks reuse the same detector → formatter → notifier pipeline, ensuring that interactive prompts and permission requests are also turned into push alerts.
export async function handleChatMessage({ message }) {
if (detector.isInteractivePrompt(message)) {
const note = formatter.formatPrompt(message);
await notifier.send(note);
}
}
export async function handlePermissionAsk({ permission }) {
const note = formatter.formatPermission(permission);
await notifier.send(note);
}(The helper functions isInteractivePrompt, formatPrompt, and formatPermission live in detector.js / formatter.js and follow the same pattern as above.)
Getting Started
Installation
# Project‑local installation (recommended)
npm install --save-dev opencode-alerterConfiguration
Create a .opencodealerter.json file at the root of your project:
{
"shoutrrrUrls": ["pushover://APP_TOKEN@USER_KEY"],
"thresholdMs": 180000,
"filters": ["npm install", "git status"],
"emojiTheme": "rich"
}- shoutrrrUrls – one or more service URLs supported by Shoutrrr.
- thresholdMs – minimum command duration before a notification is sent (default 0).
- filters – simple substrings; matching commands are ignored.
- emojiTheme –
minimal,standard, orrich.
Enable the plugin
Add the plugin to your OpenCode configuration (e.g., .opencoderc):
{
"plugins": ["opencode-alerter"]
}Now run any OpenCode command as usual; notifications will appear automatically when the conditions defined above are met.
Recent Developments
f264c9a– Fixed thechat.messagehook signature, enabling reliable interactive‑prompt notifications.c4fd8c2– Added extensive testing and a customization guide, improving developer onboarding.138c5a6– Updated the Pushover URL parser to correctly handle the@separator (required for theAPP_TOKEN@USER_KEYformat).3894979– Migrated the codebase to native ES modules, allowing modern import syntax and better tree‑shaking.
These changes polish the core experience without altering the public API; the plugin remains a drop‑in addition to any OpenCode project.
Conclusion
OpenCode Alerter bridges the gap between long‑running CLI tasks and real‑time awareness on your mobile device. By leveraging the OpenCode plugin system, it cleanly intercepts command lifecycle events, applies smart filtering, and delivers concise, emoji‑rich notifications through any Shoutrrr‑compatible service. Whether you’re running heavy builds, awaiting user input, or handling permission requests, the plugin ensures you never miss a critical update again. Install it today, tune the configuration to your workflow, and let the notifications keep you in the loop.