RCC is a Telegram bot and a local daemon. It lets one operator watch, steer, and approve the actions of Codex coding agents that run on their own Mac.
Coding agents stop and ask before they run commands or change files. If the person who started the task is away from the desk, the task waits. RCC sends each request to the operator's private Telegram chat with the project, thread, and action, so the operator can approve or deny it from a phone. The code and the files stay on the Mac.
- About 21,000 lines of TypeScript in 61 files. 13,875 lines are in
src/(54 files). 4,509 lines are in one integration test file. - 170 test cases in 5 test files. All pass on macOS with Node 22.
- 12 SQLite tables, created by the migrations in
src/db/migrations.ts. The database runs in WAL mode. - 21 commands registered with the Telegram bot menu at startup.
- 5 policy checks: project allowlist, branch push, model switch, attachment path, and artifact send.
- 0 npm packages in a
dependenciesblock. The application code imports only Node.js built-in modules. The start and test scripts usetsxand TypeScript, which are dev dependencies. - MIT license.
On a Mac with git and Node 22.5 or newer, run this in Terminal:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/PATELOM925/Remote-Codex-Control-Public/main/install.sh)"The installer clones RCC into ~/Remote-Codex-Control, installs the packages, and starts the guided setup. It is safe to run again.
To do each step yourself:
git clone https://github.com/PATELOM925/Remote-Codex-Control-Public.git
cd Remote-Codex-Control-Public
npm ci && npm run setupThe setup asks for three things:
- The bot token from @BotFather.
- Your Telegram chat. Send your new bot a message, then confirm that the chat is yours.
- The project folder the agent may work in. You can add more than one.
It writes setup.md with owner-only permissions. Then run ./run.sh. Codex must be installed and signed in for real use. RCC itself calls only the Telegram API, unless you turn on the experimental relay. Codex still talks to its own model service, as it does without RCC.
Watch it in a browser: iampatelom.com/demos/remote-codex-control replays the session below step by step.
This replays an operator session against the daemon with mock Codex and Telegram clients. It needs no bot token, no Codex install, and no network.
npm ci && npm run demooperator> /status
rcc> RCC Status
Daemon: alive
Codex Transport: connected
...
operator> /projects
rcc> Projects (1)
1. demo-app | ~/demo-app
operator> /new 1 Demo thread
rcc> Running
...
Codex ID: <id-2>
operator> /approval_demo
rcc> Demo approval triggered for Demo thread
Approval ID: <id-3>
...
Actions: /approve <id-3> | /deny <id-3>
operator> /approvals
rcc> Approvals
Pending actionable: 1
1. <id-3>
...
operator> /approve <id-3>
rcc> Approval resolved
...
Approved request <id-3>
operator> /approve <id-3>
rcc> Callback already processed: approve:<id-3>
operator> /deny <id-3>
rcc> Request <id-3> already approved
operator> /approve does-not-exist
rcc> Command failed: Unknown approval does-not-exist
- Approval routing: a demo approval card reaches the operator chat, and /approve or /deny resolves it by ID.
- Idempotent callbacks: a repeated /approve is answered as already processed and does not act twice.
- Stale or unknown approvals are rejected: a resolved request cannot change, and an unknown ID gets an error.
Random IDs, times, and ages show as placeholders such as <id-1>, <time>, and <age>. The temp folder shows as <tmp>. ... marks trimmed lines.
flowchart LR
op["Operator phone<br/>Telegram app"] --> tg["Telegram Bot API<br/>long polling"]
tg --> ta["telegramBotApiClient.ts"]
ta --> cs["telegramCommandService.ts<br/>chat allowlist"]
cs --> br["codexBridgeAgent.ts"]
br -->|"JSON-RPC over stdio"| ca["codex app-server<br/>on this Mac"]
ca -->|"approval requests"| ar["approvalRouterAgent.ts"]
ar --> pe["policyEngine.ts"]
ar -->|"approval card"| tg
br --> db[("SQLite WAL<br/>eventJournal.ts")]
src/adapters/telegram/telegramBotApiClient.tspolls the Telegram Bot API withgetUpdatesand sends replies back.src/services/telegramCommandService.tsdrops any update from a chat that is not on the allowlist. It then parses commands such as/run,/say, and/approve.src/adapters/codex/codexAppServerClient.tsstartscodex app-server --listen stdio://. It writes newline-delimited JSON-RPC messages, such asturn/start,turn/steer, andturn/interrupt.src/agents/codexBridgeAgent.tsstarts and resumes Codex threads. At startup it checks that the App Server supports the 8 methods it needs.src/agents/approvalRouterAgent.tsadds project and thread context to each approval. It rejects expired or superseded approvals. It answers a repeated button press without acting on it twice.src/policies/policyEngine.tsruns the policy checks. The artifact, attachment, git, and model-switch code paths call it.src/db/database.tsandsrc/event-journal/eventJournal.tsstore operator-visible state in SQLite. The database runs in WAL mode.src/utils/redaction.tsmasks secret-looking text before the bridge and the local status page show it.
- Language and runtime: TypeScript 5.9 on Node.js. Tests run with
node:testthroughtsx. - Codex integration: Codex App Server over JSON-RPC on stdio, the default mode.
- Operator interface: Telegram Bot API, long polling, over
fetch. - State: SQLite through
node:sqlite, in WAL mode. - Local status page: a small HTTP server bound to
127.0.0.1. - Experimental: a static web console in
apps/web, a relay function inapi/relay.js, and a Supabase schema insupabase/rcc_relay_schema.sql. - Optional always-on mode: a macOS LaunchAgent with
caffeinate, fromscripts/install-launchd.sh.
Tested on macOS with Node 22:
git clone https://github.com/PATELOM925/Remote-Codex-Control-Public.git
cd Remote-Codex-Control-Public
npm ci
npm run typecheck
npm run build
npm test
npm run verify:quickA local run with no Telegram bot and no Codex App Server. Codex and Telegram are both replaced by mock clients:
HOME="$(mktemp -d)" \
RCC_TELEGRAM_TRANSPORT=disabled \
RCC_CODEX_CONNECTION_MODE=mock \
RCC_OPERATOR_CHAT_ID=local-test \
RCC_LOCAL_STATUS_PORT=4799 \
RCC_RELAY_ENABLED=false \
node --import tsx src/main.ts
curl http://127.0.0.1:4799/api/healthSet HOME to an empty folder, as shown, so the thread list does not read your real ~/.codex history. The daemon creates a .dist/ folder inside the repo. That folder is git-ignored.
The real flow needs two more things:
- The real Telegram flow needs
setup.mdwith a bot token from BotFather and your numeric chat ID. Runnpm run setupto write it, or copysetup.example.mdby hand. Then run./run.sh. Ifsetup.mdis missing,run.shrestores it from.dist/setup.backup.mdwhen that backup exists. Otherwise it exits with an error. - A real Codex session needs the Codex app or CLI installed and signed in on the Mac.
npm test runs the 170 test cases in the 5 files under tests/. The run takes about 10 to 20 seconds. The examples below are in tests/rcc.integration.test.ts, and the line numbers refer to that file:
- Unknown chats are rejected (line 1204).
- A git push to
mainis blocked (line 1052). - Stale approvals expire, and duplicate callbacks are idempotent (lines 1625 and 2567).
- Superseded approvals are rejected (line 3240).
- Attachment names with path traversal stay inside the thread folder (line 311).
- Artifact sends re-check that the file is inside the artifact root (line 2947).
- Secret-looking text is redacted (line 3148).
- The local status server refuses a non-local bind address (line 1030).
- The setup wizard accepts only a confirmed private chat, keeps the old
setup.mdas a backup, and never prints the bot token.
A GitHub Actions workflow runs the typecheck, the tests, the secret scan and the web demo build on every push to main and on pull requests.
The design assumes one trusted operator, one trusted Mac user session, one private Telegram bot, and an allowlist of chat IDs. It treats Telegram messages, uploaded files, generated artifacts, and model output as untrusted.
Main controls:
- Only allowlisted chat IDs can send commands.
- An uploaded file goes to a selected or named thread. It is stored under that thread's folder.
- Artifacts are listed first. A file is sent only when you ask for it by index or with
all. - Expired and superseded approvals cannot be approved or denied.
- Approval callbacks are idempotent.
- The local status page binds to
127.0.0.1only. - The bot token lives in
setup.md, which is git-ignored.
Known limits:
- The Telegram bot token is a bearer credential. If it leaks, revoke it in BotFather.
- Secret redaction is best-effort. Do not paste secrets into Codex or RCC.
- Uploaded files can contain prompt injection.
- The Codex App Server protocol can change and break RCC.
The full threat model is in SECURITY.md and docs/THREAT_MODEL.md.
Solo project: design, implementation, security model and tests.
- Public alpha. One Mac, one operator, one private Telegram bot.
- Telegram is the supported interface. The web console and the hosted relay are experimental and are not part of the setup path.
- The web console has a public demo mode with simulated data. It does not connect to any computer. Build it with
npm run web:demo:build; seeapps/web/README.md. - A public demo of the web console with simulated data runs at https://rcc-demo.iampatelom.workers.dev
- The demo is a static site with no backend. Its Cloudflare config is in
deploy/web-demo, and deploying it is a manual step. - The hosted relay is single-operator and off by default. It needs an explicit
https://URL inRCC_RELAY_BASE_URL. A loopbackhttp://URL is allowed for local tests. Do not share its URL or operator token with other people. - Desktop UI approval automation is a prototype and is off by default.
- No multi-user access and no cloud execution.
- Node 22 prints an
ExperimentalWarningfornode:sqlite. - The bot shows only reasoning summaries that the App Server exposes. It does not show hidden reasoning.
- Install: local Telegram setup.
- Quickstart: a first run in Telegram.
- Examples: common operator flows.
- Architecture: daemon, Telegram, Codex, state, and trust boundaries.
- Architecture diagram: Mermaid diagrams and trust boundaries.
- Telegram bot spec: the command contract.
- Setup reference: every
setup.mdkey. - Security and threat model.
- Always-on Mac runtime: LaunchAgent and sleep limits.
- Contributing.
- License: MIT.