Control and monitor OpenCode from Telegram. Install it as the npm package @coinseeker/opencode-telegram-plugin@latest, then configure your Telegram bot credentials in a private local env file.
Disclaimer: This project is not affiliated with, endorsed by, or sponsored by OpenCode, SST, or any of their affiliates. OpenCode is a trademark of SST.
- Task completion notifications: Receive Telegram messages when the root OpenCode session is truly finished.
- Subagent-aware completion: Child/subagent idle events are suppressed, and parent completion is deferred until background subagents finish.
- Question replies from Telegram: Answer OpenCode
questionprompts from inline Telegram buttons. - Multi-select question replies: Toggle multiple choices in Telegram and submit them with Done.
- Custom answers: Use Telegram free-text replies for prompts that allow custom input.
- Permission alerts: Receive a ping when OpenCode is waiting on a permission decision.
- Permission replies from Telegram: Approve once, always allow, or reject OpenCode permission prompts from Telegram.
- Multi-session safe: A file-lock leader/pass-through model prevents duplicate Telegram polling across concurrent OpenCode windows.
- Clean terminals: Plugin logs go to a temp file instead of stdout, so OpenCode terminal output stays clean.
- Access control: Only whitelisted Telegram user IDs can interact with the bot.
- Node.js 18+
- npm
- OpenCode CLI installed
- Telegram Bot (from @BotFather)
Paste below into your OpenCode.
Install and configure OpenCode Telegram Plugin by following the instructions here:
https://github.com/ghraw/coin-seeker/opencode-telegram-plugin/refs/heads/main/docs/installation.md
OpenCode installs and loads the npm package on startup.
The @latest tag installs the newest published release, so a first-time install never needs a version pin. To see what @latest currently points to, run npm view @coinseeker/opencode-telegram-plugin version. OpenCode caches each resolved install, so updating an existing copy takes one extra step — see Updating an Existing npm Install.
For a step-by-step manual install guide and an AI-agent-friendly install prompt, see docs/installation.md.
One env file. One config entry. Restart. Done.
- Talk to @BotFather
- Create a new bot with
/newbot - Save the bot token
- Open your bot in Telegram
- Tap Start
- Send any message to establish the chat
- Send any message to @userinfobot
- Save your numeric user ID
Create ~/.config/opencode/telegram-remote/.env:
mkdir -p ~/.config/opencode/telegram-remote
chmod 700 ~/.config/opencode/telegram-remote
cat > ~/.config/opencode/telegram-remote/.env <<'EOF'
TELEGRAM_BOT_TOKEN=your_bot_token_here
TELEGRAM_ALLOWED_USER_IDS=123456789,987654321
# Optional: skip the first-message discovery step
# TELEGRAM_CHAT_ID=your_chat_id_here
EOF
chmod 600 ~/.config/opencode/telegram-remote/.envKeep this file private. Never commit or share your Telegram bot token.
Open ~/.config/opencode/opencode.json and add the pinned npm package name to the plugin array:
{
"plugin": [
"@coinseeker/opencode-telegram-plugin@latest"
]
}The plugin key is a singular string array. Each entry is either an npm package name or a file:// absolute path. There is no plugins (plural) key and no {name, path} object format.
Restart OpenCode after changing this file.
OpenCode installs the @latest entry the first time it resolves the plugin and then caches that install under ~/.cache/opencode/packages/. A plain restart does not pull a newer release, because OpenCode reuses the cached copy instead of re-querying npm. To move an existing install to the newest published release, do one of the following:
-
Refresh the
@latestcache — delete the cached package directory, then restart OpenCode:rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/opencode/packages/@coinseeker/opencode-telegram-plugin@latest" -
Or pin an exact version — set the entry to a specific version (find the newest with
npm view @coinseeker/opencode-telegram-plugin version). A new version spec is a different cache key, so OpenCode installs it fresh on the next restart.
Keep all other plugin entries unchanged, then restart OpenCode. npm package plugins are resolved only when OpenCode starts, so running sessions keep the previous version until restart.
- Start OpenCode from any project where the global config is loaded.
- Send any message to your Telegram bot in a private chat.
- The bot should reply with a chat connection confirmation.
- Run an OpenCode task and confirm you receive notifications.
OpenCode often runs in multiple terminal windows at the same time. Without coordination, each window would start its own Telegram long-poll loop, causing duplicate notifications and Telegram API conflicts.
The plugin uses a file-lock leader/pass-through model to solve this:
The first OpenCode process to start acquires an exclusive lock file at:
${os.tmpdir()}/opencoder-telegram-<sha256(token).slice(0,16)>.lock
The lock file contains the owning process's PID. The leader is the only process that runs the Telegram polling loop and receives incoming messages.
Any subsequent OpenCode process detects the existing lock and enters pass-through mode. Pass-through processes:
- Do not start a polling loop
- Can still send outbound Telegram notifications via
bot.api.sendMessage - Read the active chat ID from the shared state file (see below)
If the lock file is older than 5 minutes, or the PID it contains is no longer running, any process can reclaim the lock and become the new leader.
The active chat ID is persisted to:
~/.config/opencode/telegram-remote/state.json
This lets pass-through processes send notifications even before the user has messaged the bot in the current leader's session.
- Start OpenCode with the plugin registered
- Open your Telegram bot and send any message (e.g., "Hello")
- The bot replies with your chat ID and confirms the connection
- You're ready to receive notifications
The plugin reacts to these OpenCode events:
| Event | When it fires | Telegram behavior |
|---|---|---|
session.status / session.idle |
Root session finishes and no child/subagent is running | Sends a ✅ 작업 완료 notice with the session title and agent name (e.g. build) when known |
| Child/subagent idle | A subagent finishes | Suppressed; no Telegram completion message |
| Parent idle while background subagent is running | Parent appears idle before background work completes | Defers the parent completion message |
permission.updated |
OpenCode is waiting on a permission decision | Sends a 🔐 권한 요청 notice with the session title and permission details |
permission.asked |
OpenCode is waiting on a permission decision | Sends approve/reject inline buttons |
question.asked |
OpenCode asks an interactive question | Sends Telegram inline buttons and optional custom answer flow |
question.replied |
The question was answered elsewhere | Cleans up pending Telegram question state |
The plugin also consumes session.created and session.updated internally to cache session titles and parent/child relationships.
Control OpenCode sessions directly from Telegram.
| Command | Description |
|---|---|
/sessions |
List recent root sessions (most-recent first, top 20) |
/status <N> |
Show session #N details: agent, status, last messages, plan progress, boulder state |
/start_work <N> |
Trigger /start-work on session #N (safety-gated) |
/help |
Show command reference |
Session numbers come from the most recent /sessions call. The mapping snapshot expires after 1 hour — re-run /sessions to refresh.
/start_work <N> only dispatches opencode's /start-work command when ALL conditions are met:
agentisplan,prometheus, or a Prometheus Plan Builder label such asPrometheus - Plan Builder— session must be a planning sessionstatus === 'idle'— re-validated via live API call (TOCTOU-safe)- An incomplete
.omo/plans/*.mdplan file exists in the session's project - No
.omo/boulder.jsonfile exists (prevents duplicate execution)
When a condition fails, a specific Korean reason is returned.
Every OpenCode process writes root-session metadata to a shared local registry under ~/.config/opencode/telegram-remote/session-registry/. /sessions is handled by the leader process, but it merges the leader's live session list with that shared registry so recent root sessions from other OpenCode windows are still visible.
Note: Telegram bot commands use underscores:
/start_work(Telegram) maps internally to opencode's/start-workslash command.
When OpenCode asks a question, the bot sends the question with inline buttons.
- Tap an option to answer with that option label.
- For multi-select prompts, tap options to toggle them, then tap Done to submit.
- Tap Custom answer when available, then reply to the Telegram force-reply prompt with free text.
- Multi-question prompts are handled one question at a time.
When OpenCode asks for permission, such as reading a protected .env file, the bot sends inline buttons:
- Allow once approves only this request.
- Always allow approves this request and asks OpenCode to remember the matching rule when supported.
- Reject denies the request.
- Interrupted sessions are not reported as successful completion notifications.
- Subagent/child session completion notifications are suppressed.
- If a parent session becomes idle while background subagents are still running, the plugin waits and sends one parent completion notification after the background work is actually done.
Add your chat ID to ~/.config/opencode/telegram-remote/.env to skip the first-message discovery step:
TELEGRAM_CHAT_ID=your_chat_id_hereYou can get your chat ID by messaging the bot once, or using @userinfobot.
| Variable | Required | Description | Example |
|---|---|---|---|
TELEGRAM_BOT_TOKEN |
Yes | Bot token from @BotFather | 123456:ABC-DEF... |
TELEGRAM_ALLOWED_USER_IDS |
Yes | Comma-separated numeric user IDs | 123456789,987654321 |
TELEGRAM_CHAT_ID |
No | Pre-configured chat ID (skips discovery) | 123456789 |
TELEGRAM_IDLE_SETTLE_DELAY_MS |
No | Quiet period (ms) the root session must stay continuously idle before a completion notification is sent. Suppresses false completion pings when a harness (e.g. oh-my-opencode) re-triggers the session after background subagents, todo-continuation, or context compaction. Set to 0 to disable. Default 12000. |
12000 |
The correct schema uses "plugin" (singular) with string entries:
{
"plugin": ["@coinseeker/opencode-telegram-plugin@latest"]
}For local development from a clone, a file:// absolute path is also supported:
{
"plugin": [
"file:///Users/<your-username>/path/to/opencode-telegram-plugin/plugin/dist/telegram-remote.js"
]
}- Only whitelisted user IDs can interact with the bot
- The whitelist is comma-separated in
~/.config/opencode/telegram-remote/.env - Non-whitelisted users are silently ignored
- Use a private chat with the bot (not a group)
- Keep the bot token secret and out of version control
- Only add trusted users to the whitelist
- Check env file permissions:
chmod 600 ~/.config/opencode/telegram-remote/.env
- Verify the bot token is correct in
~/.config/opencode/telegram-remote/.env - Confirm your user ID is in
TELEGRAM_ALLOWED_USER_IDS - Make sure you've sent at least one message to the bot to establish the chat
- Check the plugin log file (see below)
- This shouldn't happen with the leader/pass-through model, but if it does, check whether a stale lock file exists at
${os.tmpdir()}/opencoder-telegram-<hash>.lockand delete it
- Confirm OpenCode is loading the current
@coinseeker/opencode-telegram-pluginpackage. - Restart OpenCode after updating the package.
- Check the log file for
failed to send question reply.
- Update the package and restart OpenCode.
- Check the log for
suppressing child session idle notification. - For background subagents, check for
deferring parent idle notification - child sessions still runningfollowed bysending deferred parent idle notification.
- Make sure you're using a private chat (not a group)
- Send any message to the bot to trigger chat discovery
- If using
TELEGRAM_CHAT_IDin the env file, verify the ID is correct
- Your user ID must be in
TELEGRAM_ALLOWED_USER_IDS - Use @userinfobot to verify your numeric user ID (not username)
The plugin writes all diagnostic output to a buffered log file. It never writes to stdout, so your OpenCode terminal stays clean.
Log file:
${os.tmpdir()}/opencoder-telegram.log
# macOS example: /var/folders/.../opencoder-telegram.log
Lock file (one per bot token):
${os.tmpdir()}/opencoder-telegram-<sha256(token).slice(0,16)>.lock
State file (persists active chat ID across sessions):
~/.config/opencode/telegram-remote/state.json
To tail the log in real time:
tail -f $(ls /tmp/opencoder-telegram.log 2>/dev/null || echo "/var/folders/*/opencoder-telegram.log")Or on macOS, find the exact path with:
node -e "const os=require('os'); console.log(os.tmpdir() + '/opencoder-telegram.log')"git clone https://github.com/coin-seeker/opencode-telegram-plugin.git
cd opencode-telegram-plugin
npm install
cd plugin
npm install
npm run buildFor local testing, add the built file to ~/.config/opencode/opencode.json with a file:// absolute path. Restart OpenCode after rebuilding. The file:// path stays the same as long as the clone location does not change.
opencode-telegram-plugin/
├── .env # Optional dev credentials (repo root, gitignored)
├── .env.example # Template for .env
├── plugin/
│ ├── src/
│ │ ├── telegram-remote.ts # Plugin entry point, event routing
│ │ ├── bot.ts # Grammy bot setup and manager
│ │ ├── config.ts # Config loading (via env-loader)
│ │ ├── events/
│ │ │ ├── session-idle.ts # Handles idle/status completion notifications
│ │ │ ├── question-asked.ts # Sends Telegram question prompts and replies
│ │ │ ├── question-replied.ts # Cleans up answered question prompts
│ │ │ ├── session-created.ts # Tracks session parent/title info
│ │ │ ├── session-updated.ts # Tracks session parent/title info
│ │ │ ├── permission-updated.ts # Handles permission.updated → notification
│ │ │ ├── types.ts # Shared TypeScript types
│ │ │ └── index.ts # Re-exports all handlers
│ │ ├── lib/
│ │ │ ├── lock.ts # File-lock leader election
│ │ │ ├── claim.ts # Per-event cross-process dedup
│ │ │ ├── pending-questions.ts # Persisted Telegram question state
│ │ │ ├── abort-tracker.ts # Interrupt notification suppression
│ │ │ ├── state-store.ts # Atomic JSON state persistence
│ │ │ ├── logger.ts # Buffered file logger
│ │ │ └── env-loader.ts # Multi-source .env loader
│ │ └── services/
│ │ └── session-title-service.ts # Session title/parent/status cache
│ ├── dist/ # Built output (gitignored)
│ ├── package.json
│ ├── tsconfig.json
│ └── tsup.config.ts
cd plugin
npm run build # Production build
npm run dev # Watch mode
npm run typecheck # Type checking only
npm test # Unit tests- Build the plugin:
npm run build - Make sure
.envexists in the repo root, or~/.config/opencode/telegram-remote/.envexists, with valid credentials - Add the
file://path to your~/.config/opencode/opencode.json - Start OpenCode and message the bot to establish the connection
- Run an OpenCode task and wait for the idle notification
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Run
npm run typecheck,npm test, andnpm run build - Submit a pull request
MIT