Mirror Codex app-server sessions into Telegram forum topics or Discord project channels.
Each Codex thread gets one Telegram forum topic or one Discord text channel. Messages from Codex are mirrored into that destination, and replies from allowed chat users are routed back into the same Codex thread. The bridge also supports chat approval buttons for Codex permission requests.
This project is intended for Codex users only right now. The easiest setup path is to open this repository in Codex and ask Codex to read LLMs.txt; that file is written as an agent setup guide.
- Telegram: one forum topic per Codex thread.
- Discord: one text channel per Codex thread, grouped under a category named after the project/worktree, for example
erp/main. - Chat replies route back to the matching Codex thread.
/newor!codex newcreates a new Codex thread and chat destination.- CLI-created Codex sessions are detected and mirrored.
- Codex completion notices are sent with high priority so long transcript backlogs do not hide that a task is done.
- Optional Watch API for iPhone/watchOS clients to start Codex sessions and poll compact progress.
- Allowlist-based Telegram and Discord control.
- Inline approve, decline, and cancel buttons for Codex approval requests.
- Operational commands for status, resync, pause/resume, relink, rename, unlink, logs, and topic cleanup.
- Local JSON state file; Telegram and Discord can be bound at runtime, with optional Discord env pre-bind.
- Node.js 20 or newer.
- Codex CLI installed and authenticated on the machine running the bridge.
- A Telegram bot token from BotFather.
- A Telegram supergroup with forum topics enabled.
- The bot added to that group as an admin with permission to create/manage topics and send messages.
- Your Telegram numeric user id for
TELEGRAM_ALLOWED_USER_IDS. - Or a Discord bot token, a Discord server, and your Discord numeric user id for
DISCORD_ALLOWED_USER_IDS. - For Discord, enable Message Content Intent in the Discord Developer Portal and grant the bot
Manage Channels,View Channels,Send Messages, andRead Message History.
Open Codex and paste this prompt:
Clone https://github.com/yhdesai/codex-toolbox, read LLMs.txt, and set up Codex Toolbox in this Codex instance. Install dependencies, run tests, configure the required environment variables using the values I provide, start the Telegram or Discord bridge, and tell me what to do in chat.
Codex should then follow LLMs.txt to install, test, configure, and run the bridge. You will still need to provide either Telegram or Discord bot credentials, your numeric chat user id, and a server/group where the bot has the required permissions.
Clone and install:
git clone https://github.com/yhdesai/codex-toolbox.git
cd codex-toolbox
npm installSet runtime environment for Telegram:
export TELEGRAM_BOT_TOKEN=replace-me
export TELEGRAM_ALLOWED_USER_IDS=<telegram-user-id>Or for Discord:
export CODEX_SYNC_PROVIDER=discord
export DISCORD_BOT_TOKEN=replace-me
export DISCORD_ALLOWED_USER_IDS=<discord-user-id>
export DISCORD_GUILD_ID=<discord-server-id>
export DISCORD_PROJECT_NAME=codex-toolboxStart the bridge:
npm startIn your forum-enabled Telegram group, send:
/bind
Then either start a new Codex session normally, or create one from Telegram:
/new Investigate bug
To start the new Codex thread in a specific directory:
/new --cwd /absolute/path/to/project Investigate bug
For Discord, invite the bot to your server, then run:
!codex bind
!codex new Investigate bug
Environment variables:
TELEGRAM_BOT_TOKEN=replace-me
TELEGRAM_ALLOWED_USER_IDS=<telegram-user-id>[,<telegram-user-id>]
DISCORD_BOT_TOKEN=replace-me
DISCORD_ALLOWED_USER_IDS=<discord-user-id>[,<discord-user-id>]
DISCORD_GUILD_ID=<discord-server-id>
CODEX_SYNC_PROVIDER=telegram|discord
DISCORD_PROJECT_NAME=workspace-folder-name
DISCORD_COMMAND_PREFIX=!codex
CODEX_APP_SERVER_COMMAND=codex
CODEX_APP_SERVER_ARGS="app-server proxy"
CODEX_APP_SERVER_CWD=/path/to/workspace
CODEX_TOOLBOX_STATE=~/.codex-toolbox.json
CODEX_MESSAGE_SCOPE=all|conversation|none
CODEX_TELEGRAM_MESSAGE_SCOPE=all|conversation|none
CODEX_DISCORD_MESSAGE_SCOPE=all|conversation|none
CODEX_TELEGRAM_POLL_MS=5000
CODEX_TELEGRAM_PRIVATE_INTERVAL_MS=1000
CODEX_TELEGRAM_GROUP_INTERVAL_MS=3200
CODEX_TELEGRAM_GLOBAL_INTERVAL_MS=40
CODEX_WATCH_API_PORT=8787
CODEX_WATCH_API_HOST=127.0.0.1
CODEX_WATCH_API_TOKEN=replace-me
CODEX_WATCH_PROJECTS="codex-toolbox=/path/to/codex-toolbox,web=/path/to/web"Defaults:
CODEX_APP_SERVER_COMMAND:codexCODEX_APP_SERVER_ARGS:app-server proxyCODEX_SYNC_PROVIDER:discordifDISCORD_BOT_TOKENis set, otherwisetelegramDISCORD_PROJECT_NAME: workspace folder nameDISCORD_COMMAND_PREFIX:!codexDISCORD_GUILD_ID: optional server id to pre-bind at startupCODEX_MESSAGE_SCOPE: optional default mirrored Codex message scope for both providersCODEX_TELEGRAM_MESSAGE_SCOPE: Telegram mirrored Codex message scope; defaults toconversationCODEX_DISCORD_MESSAGE_SCOPE: Discord mirrored Codex message scope; defaults toall- Message scope values:
allmirrors every supported Codex event, tool output, approval, and status notice;conversationmirrors only user and agent/assistant messages;nonedisables mirrored Codex transcript messages while keeping commands and replies usable. CODEX_TOOLBOX_STATE:~/.codex-toolbox.jsonCODEX_TELEGRAM_POLL_MS:5000CODEX_TELEGRAM_PRIVATE_INTERVAL_MS:1000, one outbound Telegram message per second per private chatCODEX_TELEGRAM_GROUP_INTERVAL_MS:3200, about 20 outbound Telegram messages per minute per groupCODEX_TELEGRAM_GLOBAL_INTERVAL_MS:40, a conservative global Bot API pacing delay between outbound callsCODEX_WATCH_API_PORT: unset by default; set it to enable the iPhone/watchOS HTTP APICODEX_WATCH_API_HOST:127.0.0.1; a token is required if this is not loopbackCODEX_WATCH_API_TOKEN: optional bearer token for the Watch API; strongly recommended for any non-local tunnelCODEX_WATCH_PROJECTS: comma-separated project list for the app, either/path/to/projectorname=/path/to/projectTELEGRAM_ALLOWED_USER_IDS: empty, which means nobody can control the bridgeDISCORD_ALLOWED_USER_IDS: empty, which means nobody can control the Discord bridge
CODEX_TELEGRAM_STATE and the codex-telegram-topic-sync binary name are still accepted as legacy aliases for existing installs.
If codex app-server proxy does not initialize on your host, use direct stdio:
export CODEX_APP_SERVER_ARGS="app-server"npm install -g pm2
TELEGRAM_BOT_TOKEN=replace-me \
TELEGRAM_ALLOWED_USER_IDS=<telegram-user-id> \
CODEX_APP_SERVER_ARGS="app-server proxy" \
pm2 start bin/codex-toolbox.js --name codex-toolbox --update-env
pm2 saveRestart with updated environment:
TELEGRAM_BOT_TOKEN=replace-me \
TELEGRAM_ALLOWED_USER_IDS=<telegram-user-id> \
CODEX_APP_SERVER_ARGS="app-server proxy" \
pm2 restart codex-toolbox --update-env
pm2 saveFor Discord, use:
DISCORD_BOT_TOKEN=replace-me \
DISCORD_ALLOWED_USER_IDS=<discord-user-id> \
DISCORD_GUILD_ID=<discord-server-id> \
CODEX_SYNC_PROVIDER=discord \
DISCORD_PROJECT_NAME=codex-toolbox \
CODEX_APP_SERVER_ARGS="app-server proxy" \
pm2 start bin/codex-toolbox.js --name codex-toolbox-discord --update-env
pm2 save/bind: bind the current forum group./help: list available commands./new Optional title: choose a project and worktree, then create a Codex thread and Telegram topic./new --cwd /absolute/path Optional title: create a Codex thread in a specific directory.--diris also accepted. Paths must be absolute;~/pathis supported./topics: list currentthreadId -> message_thread_id -> titlemappings./delete_all_topics confirm: delete all Codex-mapped Telegram topics, clear mappings and approvals, and keep the group binding./unlink: remove this topic's Codex mapping without deleting the Telegram topic./relink <threadId>: map this Telegram topic to an existing Codex thread./resync: run thread discovery immediately./pause: pause Codex-to-Telegram mirroring; Telegram replies and admin commands still work./resume: resume mirroring and run discovery./rename <title>: rename this Telegram topic and attempt to rename the Codex thread./interrupt: interrupt the mapped Codex thread./status: show binding, pause state, mapped topics, known threads, approvals, cooldowns, allowed users, and recent errors./logs: show short redacted diagnostics from in-memory errors and PM2 log tails when readable.
Plain text inside a mapped topic is sent to the matching Codex thread. Plain text from an allowed user outside a mapped topic gets a guidance reply. Unauthorized text is ignored silently.
!codex bind: bind the current Discord server and create/reuse the project category.!codex help: list available commands.!codex new Optional title: choose a project and worktree, then create a Codex thread and Discord text channel under that project/worktree category.!codex new --cwd /absolute/path Optional title: create a Codex thread and Discord text channel rooted in a specific directory.--diris also accepted.!codex topics: list currentthreadId -> channel_id -> titlemappings.!codex status: show binding, pause state, project category, mapped channels, known threads, approvals, allowed users, and recent errors.!codex resync: run thread discovery immediately.!codex pause: pause Codex-to-Discord mirroring; Discord replies and admin commands still work.!codex resume: resume mirroring and run discovery.!codex rename <title>: rename this Discord channel and attempt to rename the Codex thread.!codex interrupt: interrupt the mapped Codex thread.!codex unlink: remove this channel's Codex mapping without deleting the Discord channel.!codex relink <threadId>: map this Discord channel to an existing Codex thread.!codex delete_all_channels confirm: delete all Codex-mapped Discord channels and clear their mappings.!codex delete_unlinked_channels confirm: delete Discord text channels across the whole server that are not linked to Codex threads.!codex delete_unlinked_channels project confirm: delete Discord text channels in known Codex categories that are not linked to Codex threads.!codex logs: show short redacted diagnostics from in-memory errors and PM2 log tails when readable.
Plain text inside a mapped Discord channel is sent to the matching Codex thread. Plain text from an allowed user in an unmapped channel gets guidance. Unauthorized text is ignored silently.
Discord creates categories for project/worktree roots, such as erp/main or omniflow/ISO. Each Codex session becomes a text channel inside the matching category.
Set CODEX_WATCH_API_PORT to expose a small JSON API designed for an iPhone app and Apple Watch companion. The Watch can start a Codex session, show compact progress, send a short reply, and interrupt a run. Detailed diff/log review should still happen on iPhone, Telegram, Discord, or desktop.
Native SwiftUI client source lives in apps/apple. It includes an iPhone app and a watchOS app that use this API.
Example startup:
CODEX_WATCH_API_PORT=8787 \
CODEX_WATCH_API_TOKEN=replace-me \
CODEX_WATCH_PROJECTS="toolbox=/home/yash/projects-shiprdev/codex-sync,web=/home/yash/projects-shiprdev/web" \
npm startFor remote access from iPhone/watchOS, put this behind a private tunnel or reverse proxy and keep CODEX_WATCH_API_TOKEN enabled.
Endpoints:
GET /watch/health
GET /watch/projects
GET /watch/sessions
GET /watch/sessions/:threadId
POST /watch/sessions
POST /watch/sessions/:threadId/reply
POST /watch/sessions/:threadId/interrupt
Use Authorization: Bearer <CODEX_WATCH_API_TOKEN> or X-Codex-Watch-Token: <token> when a token is configured.
Start a session:
curl -X POST http://127.0.0.1:8787/watch/sessions \
-H 'Authorization: Bearer replace-me' \
-H 'Content-Type: application/json' \
-d '{"cwd":"toolbox","prompt":"Fix the login crash and open a PR"}'Compact session response:
{
"session": {
"threadId": "thread-id",
"title": "Fix the login crash",
"cwd": "/home/yash/projects-shiprdev/codex-sync",
"status": "working",
"latest": "Prompt sent",
"events": []
}
}Watch-friendly statuses include starting, working, editing, running_command, testing, waiting, done, error, and interrupted.
The bridge mirrors human-readable events:
- User messages.
- Assistant messages.
- Plans.
- Errors.
- Approval prompts.
- Concise command and tool summaries.
The bridge does not mirror raw reasoning text or full command output chunks by default. Noisy lifecycle-only events such as thread/status/changed, turn/started, and turn/completed are suppressed.
Assistant streaming deltas are buffered and sent once when the assistant message completes. Messages that originated in Telegram or Discord are de-duplicated so the bot does not echo the same text back into the topic or channel.
For Codex CLI sessions that do not emit live app-server item events to this bridge process, the bridge polls the mapped Codex session JSONL file and mirrors new user, assistant, task-completion, tool-call, and tool-output entries.
The Telegram group is bound at runtime with /bind; no group id is hardcoded. The Discord server is bound at runtime with !codex bind, or pre-bound at startup with DISCORD_GUILD_ID.
The local state file stores:
- Bound Telegram group id.
- Bound Discord guild id and project category id.
- Codex thread to Telegram topic mappings.
- Telegram topic to Codex thread mappings.
- Codex thread to Discord channel mappings.
- Discord channel to Codex thread mappings.
- Pending approval callbacks.
- Pause state.
- Recent redacted errors.
Startup discovery does not backfill historical Codex sessions into Telegram topics. Existing sessions are recorded as a baseline. A topic is created when:
- A new Codex thread appears after startup.
/newcreates a thread./new --cwd /absolute/path Optional titlecreates a thread rooted in that directory.- An old unmapped thread shows fresh activity after startup.
/delete_all_topics confirm only deletes topics known in the bridge state file. Telegram bots cannot enumerate every arbitrary topic in a forum group.
- Do not commit real Telegram bot tokens.
- Do not commit real Discord bot tokens.
- Do not commit
~/.codex-toolbox.json; it contains group and topic ids. Existing installs may also have the legacy~/.codex-telegram-topic-sync.jsonstate file. - Keep
TELEGRAM_ALLOWED_USER_IDSnarrow. Allowed users can send messages to Codex and answer approval prompts. - Keep
DISCORD_ALLOWED_USER_IDSnarrow. Allowed users can send messages to Codex and answer approval prompts. /logsredacts token-shaped strings before sending diagnostics to Telegram, but avoid posting sensitive logs in shared groups.- The bot must be an admin to create, rename, and delete topics.
Check PM2 and logs:
pm2 list
pm2 logs codex-toolbox --lines 120 --nostreamInspect local bridge state:
sed -n '1,220p' ~/.codex-toolbox.jsonCommon issues:
Bad Request: not enough rights to create a topic: make the bot an admin with forum topic permissions, then run/bindagain.Too Many Requests: retry after N: Telegram is rate-limiting topic or message creation. The bridge queues outbound Telegram calls, paces group sends, and retries automatically after Telegram's requested delay. Completion/status notices jump ahead of normal transcript messages, so a long backlog can keep flushing after the task is already marked complete.- No Telegram response from a user: check that the user's numeric Telegram id is included in
TELEGRAM_ALLOWED_USER_IDS. - Message in group gets guidance instead of routing: send it inside a mapped forum topic, not the general group.
- CLI session topic appears but messages do not: run
/statusor/resync; the bridge tails mapped CLI session files on the discovery poll interval.
Run tests:
npm testCovered areas include:
- JSON-RPC request/response matching and reconnect.
- Persistent state.
- Telegram topic payloads, chunking, command parsing, and approval buttons.
- Topic commands:
/bind,/new,/topics,/delete_all_topics,/unlink,/relink,/resync,/pause,/resume,/rename,/interrupt,/status, and/logs. - Telegram-originated echo suppression.
- CLI session JSONL tailing.
Before publishing changes:
rg -n "TELEGRAM_BOT_TOKEN='|DISCORD_BOT_TOKEN='|[0-9]{6,}:[A-Za-z0-9_-]{20,}|/home/|boundChatId|-100[0-9]+" .
npm testThe repository should contain source, tests, and docs only. Runtime state, logs, and secrets should stay local.