botctl is a Rust CLI for keeping Claude Code, Codex CLI, and OpenCode sessions visible and controlled inside tmux.
It can launch and safely drive Claude Code panes, classify Codex CLI panes from tmux screen captures with narrow YOLO approval for command permission dialogs, and passively discover OpenCode panes for dashboard visibility, recent-message context, state classification, and tmux window status.
It can also dump the latest persisted assistant message from a Claude, Codex, or OpenCode pane to a Markdown file with last-message.
The project is built around a simple rule: terminal automation is only safe when tmux transport, live observation, classification, and action policy stay separate. Sending keys alone is not enough.
The recommended install method is Cargo:
cargo install botctlThis pulls the latest release from crates.io and places the botctl binary on your PATH (typically ~/.cargo/bin).
To build from source instead:
git clone https://github.com/colinmollenhour/botctl
cd botctl
cargo install --path .See Requirements for the runtime dependencies (tmux, plus claude for Claude automation, codex for Codex CLI visibility and permission approval, and opencode for OpenCode dashboard visibility).
These are the commands that matter most in day-to-day use:
dashboardto see Claude Code panes, screen-detected Codex CLI panes, and resolvable OpenCode panes, grouped by workspace, with state, age, and YOLO controls for Claude and Codexlast-messageto export the full latest assistant text from a pane transcript to Markdownyoloto babysit one pane or a scoped set of panes automaticallyserveto stream live observation data for one tmux session in human or JSONL form
For recovery actions, use the canonical names approve, reject, and dismiss-survey. The long names approve-permission and reject-permission remain compatibility aliases.
Everything else is mostly setup, diagnostics, recovery, or lower-level plumbing around those flows.
- launch a managed Claude Code session in tmux
- classify Codex CLI panes from captured terminal screens and approve command permission dialogs with YOLO
- passively discover OpenCode panes by matching their tmux title and cwd against OpenCode's SQLite session database
- list panes and inspect tmux metadata
- capture pane contents and classify the current UI state
- run
statusanddoctoragainst a live Claude Code or Codex CLI pane - dump the latest persisted assistant message from Claude, Codex, or OpenCode to
MESSAGE_<provider-session-id>.mdor a path passed with--out - run
serveas a foreground long-lived observer for one tmux session - run
dashboardas a popup-sized TUI across Claude Code panes, screen-detected Codex CLI panes, and resolvable OpenCode panes, grouped by workspace with per-pane YOLO controls for Claude and Codex - record and replay fixture cases for classifier regression tests
- prepare prompts and hand them off through an external-editor workflow
- run guarded higher-level actions such as prompt submission, permission approval, permission rejection, and survey dismissal
- published site:
https://botctl.readthedocs.io/en/latest/ - docs app source:
docs/ - intro:
docs/docs/intro.mdx - getting started:
docs/docs/getting-started.mdx - workflows:
docs/docs/workflows.mdx - architecture:
docs/docs/architecture.mdx - serve mode:
docs/docs/serve-mode.mdx
- Rust and Cargo
tmuxclaudeavailable onPATHfor Claude Code automationcodexpanes for Codex CLI screen classification and command permission approvalopencodepanes withOC | <session title>pane titles for passive OpenCode dashboard visibility
Run the full test suite from the repo root:
cargo testRun a single test by name:
cargo test resolves_custom_binding_keys_for_actionsFor a first useful run, open a tmux pane that is running Claude Code, Codex CLI, or OpenCode, then run:
botctl dashboardFrom there:
- Use
yolofor one Claude Code or Codex pane that is blocked on a supported permission dialog. - Use
servewhen you need a foreground event stream or localhost HTTP API. - Use
last-messagewhen you need the full latest assistant reply as Markdown.
botctl yolo --pane 0:6.0
botctl serve --session demo --format jsonl
botctl last-message --pane 0:6.0 --out -Use cargo run -- ... instead of botctl ... when running from a source checkout without installing the binary.
Pane-targeted commands accept either a raw tmux pane id like %19 or an explicit tmux pane target like 0:2.3.
Open the live dashboard across Claude Code and resolvable OpenCode panes:
cargo run -- dashboardKeep the dashboard alive in a dedicated tmux-backed popup session:
cargo run -- dashboard --persistentQuick tmux popup binding:
bind-key C-c display-popup -E -w 80% -h 40% botctl dashboard --persistentStart YOLO babysitting for one pane:
cargo run -- yolo --pane 0:6.0Run the long-lived observer for one tmux session:
cargo run -- serve --session demoDump the latest assistant message from a pane transcript:
cargo run -- last-message --pane 0:4.1
cargo run -- last-message --pane 0:4.1 --out last-agent-message.md
cargo run -- last-message --pane 0:4.1 --out -Run the observer and a localhost HTTP API for a web UI:
cargo run -- serve --session demo --http 127.0.0.1:8787 --allowed-origin http://localhost:3000Use machine-readable output for tooling:
cargo run -- serve --session demo --format jsonlThe HTTP API exposes live pane state plus interactive controls such as visible prompt options. Useful endpoints include:
GET /instancesGET /instances/%251POST /instances/%251/promptPOST /instances/%251/actions/approve-permissionPOST /instances/%251/actions/continue-sessionPOST /instances/%251/actions/auto-unstickPOST /instances/%251/interactions/2
Start a managed Claude session in the current directory:
cargo run -- start --session demoSee what pane was created:
cargo run -- list
cargo run -- list --plainInspect a managed session:
cargo run -- doctor --session demo
cargo run -- doctor --session demo --plainCapture a pane directly:
cargo run -- capture --pane %19 --history-lines 120Check the live classified state for a pane:
cargo run -- status --pane %19
cargo run -- status --pane %19 --plain--plain preserves the current line-oriented output for attach, list, status, and doctor if richer human output is added later. list, status, and doctor also support --json; --json and --plain are mutually exclusive.
The same command using tmux pane syntax:
cargo run -- status --pane 0:2.3The dashboard groups Claude Code panes, screen-detected Codex CLI panes, and resolvable OpenCode panes by workspace, shows the current classified state and age for each pane, lets you jump directly to a pane with Enter, and can toggle YOLO for Claude Code and Codex panes per pane, per workspace, or globally while it is open. While it runs, it also prefixes tmux window names with per-pane status emojis in pane-index order.
The dashboard also passively includes OpenCode panes when they can be resolved without using an OpenCode API server. A pane is included only when its tmux command is opencode, its pane title is OC | <session title>, and exactly one row in OpenCode's SQLite database matches both the pane cwd and stripped title. If OpenCode truncates the pane title with ..., botctl accepts that title as a prefix only when it is still unique within the same cwd. Missing, ambiguous, duplicate, or unreadable matches are ignored. For resolved panes, the details panel shows a bounded excerpt from recent OpenCode message/part rows. OpenCode support is dashboard/window-title visibility only; YOLO, prompt submission, and guarded keypress workflows remain Claude-only.
Codex CLI panes are included by capturing likely Codex terminal panes and requiring Codex screen text such as the OpenAI Codex header, Codex-specific prompt/approval language, or a /statusline that includes the run-state field. With that statusline enabled, Ready maps to ChatReady, while Working and Thinking map to BusyResponding. Codex YOLO can approve command permission dialogs by sending y for Yes, proceed; broader prompt submission and keybinding-based automation remain Claude-only.
Persistent mode creates or reuses a dedicated tmux session named botctl-dashboard on a separate tmux socket. It then attaches to that session, so if you launch it from tmux display-popup, tmux keeps control of popup size and closing the popup only detaches from the persistent dashboard. When launched from tmux, the persistent dashboard captures the outer tmux socket first and continues inspecting that outer server's Claude Code and resolvable OpenCode panes instead of its own dedicated dashboard pane. Inside persistent mode, pressing q also detaches instead of stopping the dashboard process.
Typical loop:
cargo run -- start --session demo --cwd /path/to/project
cargo run -- doctor --session demo
cargo run -- list
tmux attach -t demoIf the session is blocked on a known confirmation flow, target the pane directly:
cargo run -- approve --pane %19
cargo run -- reject --pane %19
cargo run -- dismiss-survey --pane %19Or with an explicit tmux pane target:
cargo run -- approve --pane 0:2.3approve-permission and reject-permission still work as aliases for older scripts.
Prepare and submit a prompt:
cargo run -- prepare-prompt --session demo --text "Summarize the current repo"
cargo run -- submit-prompt --session demo --pane %19 --text "Summarize the current repo"Scope prompt prep or babysit work to one workspace:
cargo run -- prepare-prompt --session demo --workspace . --text "Summarize the current repo"
cargo run -- yolo start --all --workspace .Show the CLI help:
cargo run -- helpbotctl respects the user's existing Claude keybindings. It resolves actions like submit, external editor, and confirmation flows from ~/.claude/keybindings.json instead of assuming that a hard-coded automation keymap is installed.
install-bindings is intentionally non-destructive. If the user already has a Claude keybinding file, botctl will merge in any missing required bindings when it can, and fail clearly on invalid JSON or key conflicts instead of overwriting the file.
Print the recommended automation keymap:
cargo run -- bindingsCreate or update the keymap with any missing required bindings:
cargo run -- install-bindingsWrite the recommended keymap to another path for inspection:
cargo run -- install-bindings --path /tmp/claude-keybindings.jsonRecord a fixture case from a live session:
cargo run -- record-fixture --session demo --case folder_trust_promptReplay a saved fixture:
cargo run -- replay --path fixtures/cases/permission_dialogMinimal release flow:
cargo fmt --check && cargo test && cargo package
git tag vX.Y.Z
cargo publish
git push && git push origin vX.Y.ZThe classifier recognizes:
ChatReadyUserQuestionPromptBusyRespondingPermissionDialogPlanApprovalPromptFolderTrustPromptSurveyPromptExternalEditorActiveDiffDialogUnknown
Recap is auxiliary metadata, not a primary state. Strong anchors like while you were away and away summary can surface it, but /recap by itself does not.
approve accepts both PermissionDialog and FolderTrustPrompt. For FolderTrustPrompt, botctl sends raw Enter because that flow must confirm the default selected option directly. approve-permission remains an alias for older scripts.
- Live classification is still built around
capture-pane, withserveusing a best-effort merged stream model when that helps breakUnknownstates. - The classifier is keyword-based and intentionally conservative.
botctlcan attach to existing Claude Code panes, but the strongest and most tested automation path is still managed Claude sessions.- OpenCode support is passive dashboard/status visibility. Codex support includes dashboard/status visibility and YOLO approval for command permission dialogs; broader guarded keypress automation remains Claude-only.
serveis an initial foreground observer, not the full daemon/API/SSE control plane described inPLANS-Serve-Mode.mdyet.