csd — Claude Session Driver
Drive an interactive claude REPL inside a detached tmux session — on the flat-rate
subscription seat — then inject prompts reliably and read session state back as JSON. Built so
other tools (copad, a life-assistant server, your own scripts) can run autonomous Claude work by
shelling out to csd and parsing its output.
┌──────────┐ spawn/send/approve ┌───────────────────────────┐
your │ csd │ ────────────────────▶ │ tmux session │
script │ (JSON) │ │ └─ interactive `claude` │
│ │ ◀──────────────────── │ (subscription seat) │
└──────────┘ state (hybrid) └───────────────────────────┘
Why
From 2026-06-15 claude -p / the Agent SDK becomes metered, so the only flat-rate substrate for
heavy autonomous agent work is the interactive REPL — which has no programmatic API. csd is
the missing driver: it scripts that TUI over tmux (spawn, input injection with readiness retries,
and a hybrid state detector) and exposes a clean JSON surface. See
docs/poc-findings.md for the empirical basis.
⚠️ Driving the interactive REPL programmatically is undocumented territory. The backend is kept swappable (
claudetoday,codexlater) so a forced move is a config flip, not a rewrite.
Install
…or from source:
&&
Requirements: tmux on PATH, and a logged-in claude CLI on the subscription seat (no
ANTHROPIC_API_KEY — that switches to metered billing).
Quick start
# 1. Spawn a detached session in a directory (auto-clear the first-run folder-trust gate)
name=
# 2. Send a prompt — csd verifies the keystrokes landed, then submits
# 3. Poll state until the plan-approval gate is up
# 4. Approve it (option 1 = "Yes, and use auto mode")
# 5. List everything you're driving, then clean up
Command reference
Every structured command prints JSON to stdout; errors print {"error": "..."} to stderr
and exit non-zero. So stdout is always safe to pipe into jq.
csd spawn
Start a detached interactive agent and track it.
| Flag | Default | Meaning |
|---|---|---|
--cwd <DIR> |
current dir | Working directory for the agent. |
--session-id <UUID> |
random v4 | Pin the session id (makes the transcript path deterministic). |
--name <NAME> |
csd-<dir>-<short> |
tmux session name. Must be [A-Za-z0-9._-], no ... |
--backend <NAME> |
claude |
Driving backend. |
--trust |
off | Auto-clear the one-time "trust this folder?" startup gate. |
--permission-mode <MODE> |
— | plan · acceptEdits · auto · bypassPermissions · default · dontAsk. |
--auto-accept |
off | Shortcut for --permission-mode acceptEdits. |
--bypass-permissions |
off | Shortcut for --permission-mode bypassPermissions (skip all permission checks). |
--yolo |
off | --dangerously-skip-permissions and auto-trust — zero friction. |
The four permission-posture flags (--permission-mode, --auto-accept, --bypass-permissions,
--yolo) are mutually exclusive.
# {
# "name": "myjob",
# "session_id": "0a1b2c3d-…",
# "cwd": "/tmp/work",
# "backend": "claude",
# "jsonl_path": "/home/you/.claude/projects/-tmp-work/0a1b2c3d-….jsonl",
# "permission_mode": null,
# "created": 1780127841
# }
csd send <session> <prompt…>
Inject a prompt. Keystrokes sent before the TUI is ready are silently dropped, so csd types the
text, verifies it echoed in the pane, retries if not, then presses Enter.
| Flag | Default | Meaning |
|---|---|---|
--no-submit |
off | Type the prompt but don't press Enter. |
--retries <N> |
5 |
Max send → verify-echo attempts. |
csd state <session>
Report the hybrid-detector verdict (always JSON). See State model.
csd approve <session> [--option N]
Answer a plan / permission / trust gate by selecting a numbered menu option (default 1).
csd ps [--json]
List tracked sessions with liveness and live state. Human table by default; --json for machines.
# {"sessions":[{"name":"myjob","session_id":"…","cwd":"/tmp/work","backend":"claude",
# "jsonl_path":"…","alive":true,"state":{"status":"working","tools":["Bash"]}}]}
csd kill <session>
Stop the session and remove its sidecar. Idempotent — a session that's already gone still gets its sidecar cleaned up.
State model
csd state (and the state field of csd ps) returns one tagged object:
status |
Extra fields | Meaning |
|---|---|---|
spawning |
— | Session is up but hasn't produced a turn yet. |
working |
tools: [] |
Assistant is streaming or a tool call is in flight. |
awaiting_answer |
question |
Assistant asked a clarifying question and is waiting. |
plan_ready |
plan_file, plan |
A plan is ready and the approval gate is on screen. |
blocked |
gate (trust|permission), prompt, options |
A TUI gate needs an answer — use csd approve. |
idle_done |
text |
Assistant finished its turn cleanly; waiting for you. |
dead |
— | The tmux session is gone. |
unknown |
— | Up but produced output csd doesn't recognize. |
Permission postures (unattended runs)
| You want… | Use | Effect |
|---|---|---|
| Approve each tool yourself | (default) + poll for blocked |
csd surfaces gates; you csd approve. |
| Auto-approve file edits only | --auto-accept |
Edits run; other tools still gate. |
| Skip all permission checks | --bypass-permissions |
No permission gates (add --trust for a new dir). |
| Maximum zero-friction | --yolo |
Skips permission checks and the folder-trust gate. |
Note: recent
claudeauto-approves read-only Bash (ls,whoami) even indefaultmode; only mutating/uncategorized commands raise a gate.
Consuming from a script
The whole point is shell-out + JSON. A minimal "ask a question, wait, answer" loop:
name=
while ; do
s=
done
How it works
A hybrid detector combines three signals — no single one is sufficient:
- session JSONL (
~/.claude/projects/<slug>/<id>.jsonl) — reliable for questions and normal turn completion, but lags the live TUI by ~one tool-call. - capture-pane — the only source for TUI-interrupt gates (plan / permission / trust) that never
reach the transcript until answered. Markers are release-dependent and live in one place
(
src/backend.rs). - filesystem — the plan markdown under
~/.claude/plans/, correlated to the session by spawn time (the dir is global).
Input injection uses tmux send-keys -l with a send → verify-echo → retry loop (a fixed delay is
not reliable). Each session is tracked by a small JSON sidecar under ~/.local/state/csd/sessions/.
Development
Verified against
claudev2.1.158. capture-pane gate markers and the plan-file naming shift between releases — re-verifysrc/backend.rsandsrc/detect/plan.rson upgrade.
License
MIT © Marshall Ku