Drive Claude Code and Codex TUIs through tmux from Python.
Project description
aimax
Drive Anthropic's Claude Code TUI and OpenAI's Codex TUI from
Python through tmux. One unified asyncio API + one friendly CLI for
both agents.
No SDK reverse-engineering. No headless mode. No browser automation. Just a real interactive TUI in a real tmux window, with a JSONL file-tail giving you the agent's live thought stream.
Why?
Both Claude Code and Codex ship great interactive TUIs but have no stable headless API. People who want to integrate them into larger systems typically end up:
- writing brittle screen-scrapers, or
- shelling out to one-shot
--bareinvocations and losing the agent's long-running conversation state.
This package takes a different approach borrowed from the imac project: run the real TUI in a tmux window and use tmux's load-buffer + paste-buffer primitives to feed it input. The agent persists its conversation to a JSONL file anyway, so we tail that file for the live output stream.
The result is a clean, async-friendly Python API that survives backend restarts (the tmux windows keep running) and integrates cleanly with WebSocket-style streaming consumers.
Features
- Two backends, one API:
TmuxClaudeCodeBackendandTmuxCodexBackendboth implement the sameAgentBackendcontract. - Real conversations: sessions stay alive across CLI invocations and even across backend-process restarts.
- JSONL thought-stream: subscribe to a per-session event bus, or pull a history snapshot and resume tailing from an exact byte offset with no duplicates and no gaps.
- Job-completion signal: the agent drops a
running.flagfile when its turn is done;is_job_goal_accomplished()andis_failed()reflect that in milliseconds. - Configurable: every hardcoded path from the original JS lives in one dataclass; environment variables override at runtime.
- Friendly CLI:
aimax create / send / pause / stop / list / status / history / stream— all with--jsonoutput for scripts. - Zero runtime dependencies: stdlib only. (Python ≥ 3.9.)
Requirements
- Python ≥ 3.9.
tmuxon$PATH.claude(for Claude Code) and/orcodex(for Codex) on$PATH.- Optional, for the proxychains path:
proxychainsplus a~/proxy_envs.bashand~/proxy_claude.conf(all configurable).
The package never installs the agent binaries — they each ship their
own installers (npm, the Codex installer, etc.).
Installation
pip install aimax
Or from source:
cd mobius/backend/agents_py
pip install -e .
The wheel weighs a few KB — there are no compiled extensions and no third-party Python deps.
Quick start (Python)
import asyncio
import aimax
async def main():
backend = aimax.get("tmux-codex")
# Spawn a fresh session and submit the first prompt.
handle = await backend.create_new_session({
"sessionId": "demo-1",
"cwd": "/tmp/sandbox",
"initialPrompt": "Make a hello.py that prints hi.",
})
print("Started, agent thread =", handle["agentSessionId"])
# Subscribe to live events.
def on_event(raw):
print("event:", raw.get("type"), raw.get("payload", {}).get("type"))
unsubscribe = backend.get_agent_raw_thought_stream("demo-1", on_event)
# Send a follow-up without interrupting.
await backend.no_pause_current_and_queue_query_at_session({
"sessionId": "demo-1",
"prompt": "Also add a README.",
})
# ... eventually ...
unsubscribe()
await backend.terminate_session("demo-1")
asyncio.run(main())
Quick start (CLI)
# Start a session
aimax create -b tmux-codex -s demo --cwd /tmp/sandbox \
-p 'Make a hello.py that prints hi.'
# Add a follow-up — won't interrupt the current turn
aimax send -b tmux-codex -s demo \
-p 'Also add a README.'
# Watch the live event stream (Ctrl-C to stop)
aimax stream -b tmux-codex -s demo
# Quick health check
aimax status -b tmux-codex -s demo
# Inspect everything that's running
aimax list
# Clean up
aimax stop -b tmux-codex -s demo
Every command accepts --json for machine-readable output, and reads
the prompt from --prompt / --prompt-file FILE / stdin (pipe).
History + live stitched together
# Dump history as JSON; the `sentinel` is a byte offset.
aimax history -b tmux-codex -s demo --json > history.json
SENTINEL=$(jq -r .sentinel history.json)
# Resume the live tail from exactly that offset — no duplicates.
aimax stream -b tmux-codex -s demo --from-sentinel "$SENTINEL"
Configuration
Every filesystem path lives in
aimax.config.TmuxAgentsConfig.
The defaults follow XDG conventions; you can override any of them via
environment variable:
| Env var | Default | Notes |
|---|---|---|
AIMAX_DATA_DIR |
$XDG_DATA_HOME/aimax |
Where JSON state lives. |
AIMAX_HOME |
~ |
Used by the default expansions |
AIMAX_CODEX_HOME |
$CODEX_HOME or ~/.codex |
|
AIMAX_CLAUDE_HUB |
imac_claude_code_agent_hub |
tmux session name |
AIMAX_CODEX_HUB |
imac_codex_agent_hub |
tmux session name |
AIMAX_CLAUDE_CONFIG |
~/.claude.json |
|
AIMAX_CLAUDE_SETTINGS |
~/.claude/settings.api.json |
|
AIMAX_CLAUDE_PROJECTS_DIR |
~/.claude/projects |
|
AIMAX_CODEX_CONFIG |
~/.codex/config.toml |
|
AIMAX_CODEX_STATE_DB |
~/.codex/state_5.sqlite |
Read-only SQLite. |
AIMAX_CODEX_SESSIONS_DIR |
~/.codex/sessions |
|
AIMAX_CODEX_DEFAULT_MODEL |
gpt-5.5 |
|
AIMAX_RIGHTCODE_ENV_FILE |
~/.codex/secrets/rightcode.env |
|
AIMAX_PROXY_ENVS_BASH |
~/proxy_envs.bash |
|
AIMAX_PROXY_CHAINS_CONF |
~/proxy_claude.conf |
|
AIMAX_RUN_PREFLIGHT |
1 |
Set to 0 to skip startup checks. |
aimax config show prints every active value plus the full
env-var map.
For programmatic configuration:
from pathlib import Path
import aimax
from aimax.config import TmuxAgentsConfig, set_config
set_config(TmuxAgentsConfig(
data_dir=Path("/var/lib/myapp/aimax"),
run_preflight=False,
))
backend = aimax.get("tmux-claude-code")
Always call
set_config(...)before the firstget(...)— the backend snapshots its paths at construction time.
API reference
aimax.get(name) -> AgentBackend
Factory. Returns a singleton instance of the named backend. Valid
names are listed in aimax.SUPPORTED_BACKENDS
("tmux-claude-code", "tmux-codex").
AgentBackend (abstract surface — implemented by both backends)
| Method | Returns |
|---|---|
async create_new_session(opts) |
{sessionId, agentSessionId, jsonlPath, startedAt} |
async no_pause_current_and_queue_query_at_session(opts) |
None |
async pause_current_and_resume_from_session(opts) |
None |
async terminate_session(session_id) |
{sessionId, killed, wasWorking} |
is_alive(session_id) |
bool |
is_working(session_id) |
bool |
is_job_goal_accomplished(session_id) |
bool |
is_failed(session_id) |
bool |
list_sessions() |
list[dict] |
get_history(session_id) |
{entries, total, truncated, sentinel} |
get_agent_raw_thought_stream(session_id, listener, opts=None) |
unsubscribe callable |
opts for create / send is a dict with camelCase keys (the
contract matches the original JS API):
| Key | Required | Notes |
|---|---|---|
sessionId |
yes | Becomes the tmux window name. |
cwd |
yes for create |
Working dir; must exist. |
initialPrompt |
yes for create |
First message to send. |
prompt |
yes for send / queue |
Message to send. |
flagRoot |
no | Anchor for running.flag. Defaults to cwd. |
model |
no | Forwarded as --model to the agent. |
useProxy |
no | Force proxychains on/off; default = admin setting. |
displayName |
no | Free-form label persisted alongside the runtime entry. |
agentSessionId |
no | Resume an existing agent thread id (uuid for Claude, thread id for Codex). |
Prompt-paste recorder (analytics hook)
from aimax.services import agent_prompt_events
def my_recorder(event):
# event = {"backend_name": "...", "session_id": "...", "content_length": int}
my_metrics.add(event)
agent_prompt_events.set_recorder(my_recorder)
After set_recorder(...), every paste through any backend will hand
the event dict to your function.
How it works (the 1-minute mental model)
┌─────────────────────────────────────────────┐
│ tmux server (hub session per backend) │
│ │
│ window: my-session │
│ ┌─────────────────────────────────────┐ │
│ │ bash -lc 'exec claude ...' │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Claude TUI │ │ │
│ │ │ ────────── │ │ │
│ │ │ > user prompt │◀──┼─┼──── tmux load-buffer
│ │ │ assistant response... │ │ │ + paste-buffer -p
│ │ │ │ │ │ + send-keys Enter×3
│ │ └─────────────────────────────┘ │ │
│ └──────────────────────────┬──────────┘ │
│ │ │
└────────────────────────────────┼────────────┘
▼
~/.claude/projects/<encoded-cwd>/<uuid>.jsonl
│
▼
jsonl_watcher.watch(...)
│
▼
your `on_entry` callback
- Input goes through bracketed paste (
paste-buffer -p) so embedded\ncannot prematurely submit; submission is viasend-keys Enterthree times (the TUI sometimes swallows the first while switching modes). - Output is tailed from the JSONL the agent writes anyway. We share one watcher per session for the event bus, plus we can open private watchers from arbitrary byte offsets for history-resumption.
- State is persisted to two JSON files (
hub-runtime.jsonandhub-archive.json) so backend restarts don't kill running agents.
Job-completion convention
Every prompt submission writes
<flag_root>/.imac/flags/<session_id>/running.flag. By contract the
agent removes that file when its job is done (success or failure),
and writes failed.flag if it failed. Then:
backend.is_job_goal_accomplished("my-session") # ⇒ True once running.flag is gone
backend.is_failed("my-session") # ⇒ True iff failed.flag is on disk
This works because the flags are real files, so any process can read them — your monitoring scripts don't need to talk to the backend process at all.
License
MIT. See LICENSE.
Acknowledgements
This package is a Python port of the JavaScript agents/ module from
the imac project. The trade-offs and gotchas captured in the
docstrings (bracketed paste, Enter swallowing, screen-scrape trust
fallback, …) were learned the hard way by the original authors.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aimax-0.1.0.tar.gz.
File metadata
- Download URL: aimax-0.1.0.tar.gz
- Upload date:
- Size: 75.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
38d61318c9b4e02e05dcd3159e7023b3968e764673004a857f1c851f31b7c1bd
|
|
| MD5 |
d20a40d0ebc7a9b7add0a1158e23546d
|
|
| BLAKE2b-256 |
5974237b0d0f28fe94585f8a10e1df625c9b7d1ddd9b1d9c96f9a5541e95684a
|
File details
Details for the file aimax-0.1.0-py3-none-any.whl.
File metadata
- Download URL: aimax-0.1.0-py3-none-any.whl
- Upload date:
- Size: 69.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b46153c30d5a3a63a7be8706ea416bb8797d390e86428e7f426e13320ae5754
|
|
| MD5 |
444a1d0b0f72a20d965ca834d0e4e5da
|
|
| BLAKE2b-256 |
2471ab14d4d7714a92d0ebe7ca27c33a4e57cd2d861391be44f330f63cf1fbe5
|