Quickstart
Five minutes from clone to a scheduled agent. For a fuller walkthrough through to your first dispatched agent, see the Getting Started guide.
1. Install
chela uses uv. The core has two small deps; the
dashboard + live terminal wall ship as a separate install (keeps the core lean). You also need tmux, git, the
claude CLI on PATH (and gh for the dispatcher's PR flow).
# core git clone https://github.com/Devail1/chelamux && cd chelamux uv sync uv run chela status # dashboard + live terminal wall (separate install — keeps the core lean) uv sync --extra dashboard uv run chela dashboard # the Telegram bridge is its own extra. Name every extra you want in ONE # command — `uv sync --extra X` replaces the env and drops the rest uv sync --extra dashboard --extra telegram
Authenticate Claude once. chela never touches credentials —
it drives the claude CLI inside your tmux windows. Log in once on the machine
(claude, then /login — or claude setup-token for a
headless token); every agent window reuses the cached ~/.claude credentials. The
whole fleet therefore runs as one Claude account and shares its 5h / 7d rate limits.
2. Make a session, schedule an agent
A tmux session is your fleet; each window is an agent (the window name is its display name).
# one tmux session whose windows are your agents tmux new-session -d -s chela -n researcher # see what chela can see uv run chela status # poke the agent every hour, then run the daemon uv run chela schedule add researcher --every 1h --prompt "Run your research cycle." uv run chela run
3. Dispatch a TODO list into PRs
Drop a WORKFLOW.md + TODO.md into a repo (copy examples/),
then point chela at it. Each open - [ ] becomes a worktree → an agent → a PR.
uv run chela dispatch /path/to/repo/WORKFLOW.md --once # one pass uv run chela dispatch /path/to/repo/WORKFLOW.md # poll forever
Concepts
tmux is the source of truth
chela holds no separate registry of agents. Discovery is tmux list-windows +
pane_current_path, read live every tick — so what chela sees is exactly what's
running right now. A window rename keeps the same agent; nothing to re-register.
One window per agent
Each window of your session (CHELA_TMUX_SESSION, default chela) is an
agent. The window name is how you target it — schedules, messages, and the dashboard all key off it.
Two ways to put agents to work
- Schedule — for long-lived agents with a standing role. chela types
a prompt into the agent's pane on an interval, a cron expression, or once at a set time. The
agent persists between pokes; give it a
CLAUDE.mdfor stable context. - Dispatch — for ephemeral, one-shot work. Each
- [ ]in aTODO.mdspawns a throwaway agent in its own git worktree that implements the item, opens a PR, and tears its window down. Many tasks run in parallel.
The wall
The dashboard streams every agent's live terminal in one grid (drag, lock, maximize),
with a context-window bar per tile and account-wide rate-limit pills — a first-class feature,
just a separate install. The wall is on by default but loopback-guarded: because
it serves writable shells, the dashboard only serves it on a 127.0.0.1 bind. A
non-loopback bind refuses it unless you set CHELA_TERMINALS_EXPOSE=true. See
Remote access & security.
Scheduling agents
A schedule pokes an agent's pane with a prompt on a cadence — the agent does the rest.
# interval: 30s / 5m / 1h / 1d chela schedule add researcher --every 1h --prompt "Run your research cycle." # cron expression chela schedule add reporter --cron "0 */8 * * *" --prompt "Post the 8-hourly summary." # one-shot at an ISO timestamp chela schedule add deployer --once "2026-06-01T09:00" --prompt "Cut the release." chela schedule list # every task + its id chela schedule remove 3 # delete by id
Standing context: the agent CLAUDE.md
A scheduled agent wakes into whatever its working directory contains. Drop a
CLAUDE.md at the root to give it a stable role — what it is, what to do each cycle,
and its boundaries (see examples/agent-template.md). The schedule supplies the
recurring nudge; CLAUDE.md supplies the identity.
Dispatching work (the headline feature)
Turn a markdown checklist into a stream of pull requests, each built by its own isolated agent.
TODO.md — the work list
Every unchecked - [ ] bullet is a work item. Append
<!-- blocked: reason --> to make the dispatcher skip a line.
## Open - [ ] Add a --version flag to the CLI - [ ] Write a docstring for the public API entry point - [ ] Add a unit test for the config loader <!-- blocked: waiting on fixtures -->
WORKFLOW.md — the config + agent brief
A YAML front-matter block configures the dispatcher; the markdown body below it is the prompt
template handed to each agent (with {{placeholders}} like {{task_title}},
{{branch_name}}, {{workspace_path}}). Put both files in the repo root.
project_key: PROJ # branches/windows are <key>-<n>, e.g. proj-1 tracker: kind: markdown # markdown TODO.md — also: gh_issues path: TODO.md # relative to this file workspace: root: ~/.chela/worktrees/proj # where per-task worktrees go base_branch: main # branch worktrees fork from + PRs target concurrency: max: 1 # tasks in flight at once agent: cmd: claude --permission-mode auto # or: bypassPermissions on a trusted repo startup_delay_seconds: 4 ready_timeout_seconds: 60 hooks: # all optional, run in the worktree # after_create: seed .claude/settings.local.json (least-privilege perms) # before_run: uv sync --quiet || true (lockfile sync / codegen) # after_done: runs in the repo when the PR merges (e.g. a deploy)
The lifecycle
Each task is keyed by a stable SHA of its source line (idempotent — a task is never picked up twice). For each open item the dispatcher:
- creates a git worktree on branch
<project_key>-<n>, forked frombase_branch; - runs the optional
after_create/before_runhooks, then spawns an agent in that worktree with your prompt body; - the agent implements the task, strikes its
- [ ]→- [x]on its own branch, pushes, and opens a PR; - the agent's last step is
chela task-finished <task_id>, which marks the runawaiting_review, records the PR URL, and kills its window; - when you merge, the struck line lands on
base_branch, the item disappears, and the run flips todoneon the next tick.
The dispatcher shape (task-list → isolated worktree → autonomous agent → PR) is an adaptation of OpenAI's Symphony pattern.
Dashboard & the wall
uv sync --extra dashboard
uv run chela dashboard # http://127.0.0.1:5001
The web UI has tabs for agents (live liveness — alive / waiting / offline), schedules, the dispatcher, and a Kanban of runs. Liveness is derived from the native session status — no heartbeat daemon.
The embedded terminal wall streams the live ttyd panes in a grid. It's
on by default but loopback-guarded: it serves writable shells, so the dashboard
only serves it on a 127.0.0.1 bind. On a non-loopback bind it's refused unless you
opt in explicitly:
CHELA_TERMINALS_EXPOSE=true uv run chela dashboard --host 0.0.0.0 # or turn the wall off entirely: CHELA_TERMINALS_ENABLED=false uv run chela dashboard
The hooks plugin (recommended)
chela works without any Claude Code hooks — it scrapes each tmux pane as a fallback. But the
event-log plugin (chela plugin) is strongly recommended: it POSTs every
tool call, prompt and permission gate to the daemon before the fact, which unlocks
lossless blocked-agent questions on Telegram (a scraped multi-question or preview
selector otherwise reaches your phone with no options), answering a question with zero
keystrokes, and the live event Feed. It fails open — if the daemon is down the hook logs a
warning and your agent carries on.
Install it straight from this repo, inside Claude Code — works out of the box on the default
dashboard port (5001):
/plugin marketplace add Devail1/chelamux /plugin install chela@chela
On a non-default dashboard port? A hook URL is a literal (Claude Code doesn't expand env vars in it), so render your own copy with the port baked in:
chela plugin --dir ~/.chela/plugin # bakes in the port the dashboard actually bound claude --plugin-dir ~/.chela/plugin # or: /plugin marketplace add ~/.chela/plugin
Context & rate-limit pills
Exact context-window usage and the 5h / 7d rate-limit pills come from Claude Code's statusLine payload, which chela caches via a tiny hook. Install it once for precise numbers (without it, the context bar falls back to a coarse transcript estimate):
chela install-statusline # prints the snippet chela install-statusline --write # writes it (won't clobber an existing one)
Keys not reaching the terminal
If Esc (or other keys) never reaches an embedded terminal, a vim-style browser
extension such as Vimium is almost certainly capturing them at the page level —
it injects into the terminal's iframe too and swallows the keypress.
Exclude the dashboard's URL in the extension's settings. In Vimium:
Options → "Excluded URLs and keys" → add the dashboard URL and leave the
Keys field blank to disable it on that site. Quick workaround: Ctrl+3
(or Ctrl+[) sends a literal Escape.
Collaborative terminals (end-to-end encrypted)
Share a live terminal over the internet — encrypted end to end, through a relay that only ever sees ciphertext.
Share a session
In the dashboard, click Share (the link icon) on any pane's header — or
Share current session from the ⋮ overflow menu on mobile. chela
mints a share and shows a join link plus a short pairing code.
Send both to whoever's joining; they open the link, paste the code, and they're in the same
live terminal with full access — watch, type, and scroll. Everyone sees each
other as live, labeled cursors and a facepile of who's watching.
Sharing is off until you set CHELA_COLLAB_RELAY
(see below) — chela never phones home. Once a relay is configured, the Share control appears on
every pane.
How the encryption works
- The pairing code is the key. It's 16 random bytes shown as base32. Both
browsers derive AES-256-GCM keys from it with HKDF-SHA256 — directional keys for the terminal
stream and a symmetric group key (
k_pres) for presence — entirely in the browser. The keys are never sent anywhere; a wrong code just fails to decrypt (you get "wrong code", not garbage). - The relay is zero-knowledge. It's a dumb WebSocket fan-out (one room per share) that broadcasts opaque frames it cannot read — it never holds a key. It sees metadata only: the room name (derived from your tmux session + window id — not a secret) and message timing/size. The code is the sole security boundary, so a guessed room just yields undecryptable frames.
- Revocable. Stop a share and the room dies and the code rotates. The topbar's active-shares indicator gives one-tap Stop / Stop-All.
Presence
Everyone in a shared session is a live, labeled cursor mapped to the terminal grid, plus a
facepile of who's watching (the host gets a ★). Cursors and names ride the same encrypted channel
(k_pres), so the relay can't see who's present or where they're pointing. It's
colorblind-safe — every cursor carries a name label, not just a color.
On a phone
Joiners on mobile get the full experience: the terminal letterboxes to fit, an on-screen keys-line (Esc / Tab / arrows / a sticky Ctrl / …) sits above the keyboard, swipe scrolls the session, and your touch shows to everyone else as a cursor.
Run your own relay
The relay is a small Cloudflare Worker (source
in chela/collab-relay/) — a dumb, opaque fan-out with one Durable Object per room.
Deploy your own and point chela at its wss:// URL so even the room-name/timing
metadata stays yours:
# deploy the relay (Cloudflare Workers) cd chela/collab-relay && npx wrangler deploy # point chela at it, then start the dashboard CHELA_COLLAB_RELAY=wss://your-relay.workers.dev uv run chela dashboard
Full access is the model. A paired joiner holds the code, so they can both watch and drive (scroll is input on a full-screen TUI). Share only with people you'd hand the keyboard to.
Command reference
One CLI, chela. Every command targets agents by their tmux window name.
Core
chela statusList the agent windows discovered in your tmux session — the source of truth.
chela runRun the daemon: scheduler tick + dispatcher + needs-input notify. Leave it running.
chela dashboard [--host] [--port]Launch the dashboard + live terminal wall (needs the dashboard install). Binds 127.0.0.1:5001.
Scheduling
chela schedule add <agent> --prompt … (--every|--cron|--once)Schedule a prompt — an interval (30s/5m/1h/1d), a cron expression, or a one-shot ISO timestamp.
chela schedule listList every scheduled task with its id.
chela schedule remove <id>Delete a task by id.
Dispatch
chela dispatch <WORKFLOW.md> [--once] [--interval N] [--dry-run]Turn each open - [ ] item into a worktree, an agent, and a PR. Polls every 60s; --once runs one tick.
chela dispatch-runsList dispatcher runs and their status.
chela task-finished <task_id>(agents call this) mark a run awaiting-review, record the PR, and kill its window.
Messaging
chela msg <agent> <message> [--from] [--priority]Drop a message into one agent's pane. Priority: critical|high|normal|low.
chela broadcast <message> [--from] [--priority]Send the same message to every other live agent at once.
Setup
chela plugin [--dir PATH] [--port N]Render the Claude Code hooks plugin (event log + zero-keystroke Telegram answers). Recommended — see above.
chela install-statusline [--write] [--force] [--settings]Print (or --write) the statusLine snippet for ~/.claude/settings.json so panes report live context + rate limits.
Configuration
All configuration is environment variables, with sensible defaults.
| Variable | Default | Purpose |
|---|---|---|
| CHELA_TMUX_SESSION | chela | tmux session chela orchestrates |
| CHELA_DIR | ~/.chela | State dir (scheduler.db, worktrees, context) |
| CHELA_SCHEDULER_POLL_INTERVAL | 30 | Daemon loop interval (s) |
| CHELA_DISPATCH_WORKFLOWS | — | Colon-separated WORKFLOW.md paths the daemon dispatches |
| CHELA_DISPATCH_TICK_INTERVAL | 60 | Dispatcher tick interval in the daemon (s) |
| CHELA_AGENT_CMD | claude | Launch command for the dashboard Start button |
| CHELA_NOTIFY_URL | — | Needs-input notification target (ntfy / Telegram / webhook) |
| CHELA_DASH_HOST / CHELA_DASHBOARD_PORT | 127.0.0.1 / 5001 | Dashboard bind address |
| CHELA_TERMINALS_ENABLED | true | Embedded ttyd terminal wall (streams live; loopback-guarded) |
| CHELA_TERMINALS_EXPOSE | false | Serve the writable wall on a non-loopback bind too (RCE risk — opt-in) |
| CHELA_COLLAB_RELAY | — (off) | Relay wss:// URL for collaborative terminal sharing (end-to-end encrypted). Empty = sharing off; chela never phones home. Details. |
| CHELA_DEFAULT_CONTEXT_WINDOW | 200000 | Window size assumed by the transcript-based context estimate (fallback) |
Needs-input notifications
When an agent's pane enters the waiting state (a permission prompt or a question),
chela fires one edge-triggered notification — so you don't have to babysit. Transport is
auto-detected from the URL:
CHELA_NOTIFY_URL=https://ntfy.sh/my-chela-topic # ntfy CHELA_NOTIFY_URL="https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>" # Telegram CHELA_NOTIFY_URL=https://example.com/hook # generic webhook
Remote access & security
chela ships with zero built-in auth, by design. The dashboard and
the ttyd terminals bind 127.0.0.1. The wall is a writable shell — exposing it
on an untrusted network is remote code execution. The tailnet is the trust boundary,
not a password.
For remote access, put the loopback dashboard behind one of:
- Tailscale —
tailscale serve 5001gives you TLS + tailnet ACLs for free (recommended). - An SSH tunnel —
ssh -L 5001:127.0.0.1:5001 host. - A reverse proxy with your own auth.
Or skip the web UI entirely: SSH/Mosh in from a mobile terminal and tmux attach -t chela
for the live panes straight from your phone.
Or run the built-in Telegram bridge —
chela telegram gives every agent window its own forum topic (1 topic = 1 window =
1 session), so you can drive or supervise any agent from your phone, two-way (text, images, and
file attachments flow both ways). It's a full bridge, distinct from the one-shot needs-input
notifications above — and it ships with chela, no separate service to run.
To share a session with someone else over the internet — rather than exposing your whole dashboard — use collaborative terminals: end-to-end encrypted, through a relay that only ever sees ciphertext.