Remote-control Claude Code or OpenAI Codex agents running inside tmux.
tmux-claude-bot drives the real agent CLI through tmux, just like a person typing in the pane. It does not replace Claude Code or Codex with an API wrapper; it gives you a durable control layer around the interactive tools you already use.
Long-running coding agents are useful, but the laptop terminal is a fragile place to babysit them. This project keeps each workspace in a named tmux session, serializes input through a queue, streams output back to chat, and lets you switch projects from Telegram, Feishu/Lark, a local CLI, or a TUI.
- Multi-project sessions - one tmux session per workspace.
- Telegram and Feishu/Lark adapters - use either chat app or both.
- Local CLI and TUI - control the same service from the machine.
- Claude Code and Codex - configure multiple agent launch commands.
- Voice prompts - optional local transcription flow for spoken requests.
- Real-time output - capture tmux pane output and stream it back.
- Serialized execution - one queue per session prevents interleaved commands.
- Directory guard - restrict projects to allowed workspace roots.
- Managed service - install with launchd on macOS or systemd on Linux.
One-line install:
curl -fsSL https://raw.githubusercontent.com/OctopusGarage/tmux-claude-bot/main/install.sh | bashOr install from npm:
npm i -g @octopusgarage/tmux-claude-bot
tmux-claude-bot setup
tmux-claude-bot runUseful commands:
tmux-claude-bot doctor
tmux-claude-bot setup --reconfigure
tmux-claude-bot tui
tmux-claude-bot service statusTelegram / Feishu / TUI / CLI
|
v
adapters/* -> core queue -> tmux send-keys
^ |
| v
renderers <- capture-pane <- Claude Code / Codex
Important boundaries:
| Layer | Responsibility |
|---|---|
| Adapters | Translate Telegram, Feishu/Lark, TUI, and CLI input into core commands. |
| Core | Route commands, serialize per-session work, enforce workspace rules, manage lifecycle. |
| tmux integration | Send keys, capture pane output, and detect idle/running state. |
| Renderers | Convert agent output into chat messages, cards, and terminal UI views. |
See docs/manual.md and docs/adr for deeper design notes.
The setup wizard creates the service config and helps bind Telegram and/or Feishu/Lark. Typical prerequisites:
- Node.js 22+
- tmux
- Claude Code CLI or OpenAI Codex CLI installed and logged in
- Telegram bot token and/or Feishu/Lark app credentials
- macOS or Linux for managed service mode
- Only configured users/groups can control sessions.
- Workspace roots are allowlisted.
- Commands flow through per-session queues.
- tmux output is captured after command dispatch, reducing stale-output confusion.
- Local service controls are exposed through a unix-domain socket, not a public network port.
npm install
npm run setup:lark
npm run doctor
npm run service:install
npm run service:uninstall
npm run dev
npm run build
npm test
npm run lint
npm run lint:typesDeeper checks:
npm run test:coverage
npm run knip
npm run mutation
npm run audit- telegram-bridge - a smaller Telegram-only bridge for one tmux target.
- octopusgarage-skills - shared skills for Claude Code and Codex.
- OctopusGarage - small tools for AI agents, local automation, and browser-native products.
All settings via .env:
| Variable | Default | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
(optional) | Telegram bot token from @BotFather. Blank = Telegram off (Feishu-only) |
LARK_ENABLED / LARK_APP_ID / LARK_APP_SECRET / LARK_ALLOWED_OPEN_IDS / LARK_DOMAIN |
(optional) | Feishu/Lark adapter — set by npm run setup:lark. At least one of Telegram/Feishu must be configured. Feishu project groups need the im:chat and im:message.group_msg scopes (see docs/commands.md) |
CLAUDE_START_COMMAND |
claude-yolo |
Command to launch Claude (a full line; may carry leading VAR=value env). Add CLAUDE_START_COMMAND_2..N (+ optional CLAUDE_START_LABEL_n) for a pick-on-start menu. Codex flavors use CODEX_START_COMMAND[_N] the same way (absent ⇒ codex disabled) — see .env.example |
IDLE_POLL_TICKS |
3 |
Consecutive idle polls before considered idle |
POLL_INTERVAL_MS |
1000 |
Milliseconds between idle polls |
MAX_OUTPUT_LINES |
200 |
Max tmux pane lines to capture |
MAX_MESSAGE_LENGTH |
3500 |
Max Telegram message size |
TELEGRAM_ALLOWED_USER_IDS |
(empty) | Comma-separated Telegram user IDs that can use the bot |
CD_ALLOWED_DIRS |
(empty) | Allowed directories for project creation |
LOG_LEVEL |
INFO |
Minimum log level written to the JSONL log (DEBUG|INFO|WARN|ERROR) |
TCB_LOG_DIR |
~/.tmux-claude-bot/logs |
Directory for structured JSONL log files (overrides the default under TCB_STATE_DIR) |
tmux session name format: tmux_proj_<absolute-path> with / replaced by -
Example: /home/user/projects/myapp → tmux_proj_-home-user-projects-myapp
The active session name is stored in .current_project (gitignored).
| Command | Description |
|---|---|
/list_projects |
List all tmux_proj_* sessions |
/current_project |
Show current project and session status |
/add_project <path> |
Create new project tmux session |
/switch_<N> |
Switch to project by number |
/remove_<N> |
Remove project session |
| Command | When | Description |
|---|---|---|
start |
session exists | Start the agent |
status |
session exists | Check if the agent is running |
peek |
session exists | Capture current tmux pane |
esc |
agent running | Send Escape key |
interrupt |
agent running | Send Ctrl-C |
exit |
agent running | Send /exit to the agent |
restart |
agent running | Restart, resuming the conversation |
clear |
agent running | Send /clear (clear context) |
compact |
agent running | Send /compact (compact context) |
enter |
agent running | Send Enter key |
up / down |
agent running | Send arrow keys |
help |
always | Show all commands |
Buttons and TUI shortcuts confirm before exit, restart, clear, or compact.
For CLI automation, use tcb control <project> <action> --yes for those actions.
When the agent is running, any text message is sent to it and the result is returned.
Voice messages are transcribed locally with mlx-whisper (Apple Silicon only) and then forwarded to the agent like any text. The feature is off until you install it — if you never use voice, you can ignore this entirely.
Enable it (two ways):
- From Telegram: send
/voice_install. The bot runs the installer, enables the feature, and persists the path to.env— no restart needed. (No-op politely if the host isn't Apple Silicon.) - On the host:
npm run whisper:install, then put the printed path intoMLX_WHISPER_BINin.env(or re-runnpm run setup:reconfigure).
What gets installed (project-managed, reproducible — nothing global):
- A project-local
.venvcreated by uv, withmlx-whisperpinned inrequirements.txt. Model weights (defaultwhisper-large-v3-turbo, ~1.5GB — the only family that supports every offered language incl. Cantonese) download from HuggingFace on first transcription; override withWHISPER_MODELin.env. - ffmpeg is required to decode audio (
brew install ffmpeg). The installer andnpm run doctorboth check for it.
If you send a voice message before enabling the feature, the bot replies with a short note telling you how to turn it on — it never fails silently.
Recognition language. whisper's auto-detect often misreads Chinese as
Japanese, so transcription forces a language — zh by default. Switch any
time from Telegram with /voice_lang <zh|en|yue|ja|es|auto> (auto re-enables detection);
it persists to .env. Override the default with WHISPER_LANGUAGE in .env.
Optional prompt translation. If you prefer writing or speaking Chinese but want the agent to receive English prompts, install the local Argos Translate package:
npm run translate:installOr install from chat with /translate_install (Telegram/Feishu); Feishu also
shows an install button in the help card when Argos is not present.
Then set PROMPT_TRANSLATE_MODE=argos, PROMPT_TRANSLATE_FROM=zh, and
PROMPT_TRANSLATE_TO=en in .env (or set the TELEGRAM_... / LARK_... /
CONTROL_... overrides for one source). Runtime controls are
/prompt_translate status|off|on [from] [to] in Telegram/Feishu and
tcb prompt-translate status|off|on [from] [to] locally; the TUI T key toggles
control zh→en. The legacy VOICE_TRANSLATE_MODE=argos_zh_en alias still works.
When enabled, chat text, voice transcriptions, TUI input, and tcb send are
translated before they enter tmux. Leave PROMPT_TRANSLATE_MODE=off for the
default behavior.
The bot decides whether the agent is running by process detection, not screen
scraping: it walks the tmux pane's process tree (pane_pid → ps) and looks for the
agent process (claude or codex). Present → running; absent → idle. This is
theme- and output-independent. (Readiness — "the agent finished loading" — is still
detected from the pane, since the process exists before it is ready for input.)
The bot writes structured JSONL logs to ~/.tmux-claude-bot/logs/tcb-YYYYMMDD.jsonl (one file per day, 30-day retention). Query them with the tcb logs CLI subcommand:
tcb logs # today's log (INFO+)
tcb logs --level WARN # WARN and ERROR only
tcb logs --session myapp # filter by tmux session
tcb logs --trace t_a1b2c3d4 # all lines from one request trace
tcb logs --chat 123456 # filter by chat id
tcb logs --channel telegram # filter by protocol adapter
tcb logs --component core.queue # filter by logger component
tcb logs --grep "timeout" # substring match on msg
tcb logs --days 3 -n 50 # last 50 lines from the past 3 days
tcb logs --json # output raw JSON linesFrom within the chat, /logs (owner-only) shows recent WARN/ERROR entries for the current session. /logs <traceId> filters to one trace; /logs N shows the last N entries.
Get a global status snapshot of all managed sessions with the tcb dashboard CLI subcommand:
tcb dashboard # human-readable snapshot of all sessions
tcb dashboard --json # raw JSON snapshot (DashboardSnapshot)Each session row shows: agent kind, busy/idle state + current-task duration, uptime, cumulative busy time, and /status usage (context %, rate-limit state). The global header shows bot uptime, version, session count, active sessions, queue depth, and which adapters (Telegram/Lark) are enabled.
From within the chat, /dashboard (owner-only) shows the same information. On Lark it is restricted to p2p (direct) messages.
The snapshot is on-demand — there is no live auto-refresh.
The bot runs as a launchd service with automatic crash recovery:
npm run service:install # install + start the launchd agent (auto-restart on crash/boot)
npm run service:uninstall # stop and remove the launchd agent
npm run doctor # verify it's healthy (single-instance check)Warning
The installer uses sed to substitute __PROJECT_DIR__ in the plist template before
copying it to ~/Library/LaunchAgents/. Do not copy the plist manually — the placeholder
will be left unresolved and the service will fail to spawn.
Features:
KeepAlive— auto-restart on crashThrottleInterval— min 10s between restarts (prevents crash loops)- Logs to
logs/launchd.out.logandlogs/launchd.err.log
| Mechanism | Behavior |
|---|---|
| Network retry | getMe retries 5× with exponential backoff (1s→30s) |
| Message retry | Handler retries 3× with linear backoff (1s, 2s, 3s) |
| Queue persistence | Unprocessed messages saved to .queue/pending.json |
| Process auto-restart | launchd KeepAlive restarts bot on crash |
| Handler isolation | Single message failure does not block queue |
Contributions are welcome. See CONTRIBUTING.md for local dev, the verification gates, and the install/deploy/release flow. In short:
- Dev:
npm install && npm run setup && npm run dev - Deploy this machine:
/deploy(Claude command) or re-run the installer - Cut a release:
/release [patch|minor|major]— gates, bumps, tags, pushes (CI publishes the GitHub Release), then redeploys + verifies this machine
Distributed under the MIT License. See LICENSE for details.
- Claude Code — the CLI this bot drives
- grammY & Feishu/Lark Node SDK — the chat adapters
- mlx-whisper & uv — local voice transcription
- Argos Translate — optional local prompt translation


