claude-pool v0.2 includes a TUI backend that drives plain claude in a pty,
without -p and without stream-json. Select it with
ClaudePool(backend="tui") or claude-pool serve --backend tui.
Everything below was observed live against Claude Code 2.1.175 (Linux/arm64,
Raspberry Pi 5) on 2026-06-12 and is covered by tests/fake_claude_tui.py.
The worker starts:
claude --session-id <uuid> --settings <settings.json> [profile flags]
Profile flags match stream-json backend semantics where the interactive CLI
supports them. In particular, system_prompt is passed as --system-prompt,
which replaces the session system prompt. It is not passed as
--append-system-prompt.
The generated settings file registers SessionStart and Stop hooks:
{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"cat >> <ready.marker>"}]}],"Stop":[{"hooks":[{"type":"command","command":"cat >> <hook.ndjson>"}]}]}}The process owns its own POSIX process group and runs on a pty. The pty output is drained only for diagnostics. Turn completion is detected from the Stop-hook file, not by screen scraping.
When no cwd is provided, the worker starts in ~/.cache/claude-pool/cwd.
Claude Code persists workspace trust by path, so this stable scratch directory
avoids paying a trust prompt for every worker. A caller-provided cwd still wins.
Readiness is detected by the SessionStart hook writing to the marker file.
The worker keeps answering a visible trust prompt with carriage return while it
waits for that hook; it does not infer readiness from pty silence or screen
text.
Prompts are written through bracketed paste, followed by carriage return:
ESC [ 200 ~ <prompt bytes> ESC [ 201 ~ \r
Observed submit timing on Claude Code 2.1.175:
- Wait at least 0.75 s after the
SessionStartmarker before the first paste on a worker. - Wait 0.3 s between the end of bracketed paste and the carriage return.
- If no Stop-hook line arrives within 3.0 s after the first carriage return, send one additional carriage return. Pressing Enter on an empty input box is a no-op; if the first carriage return was swallowed while the TUI was still processing paste insertion, the retry submits the existing prompt.
Prompt text is sanitized before it is written into bracketed paste. The policy is intentionally lossy for terminal control characters:
- Normalize CRLF and lone CR to LF.
- Preserve LF (
\n) and tab (\t). - Remove all other C0 control characters (
\x00-\x08,\x0b,\x0c,\x0e-\x1f), DEL (\x7f), and Unicode C1 controls (U+0080-U+009F).
This removes every ESC byte from prompt content, so embedded text such as
ESC [ 201 ~ cannot terminate the bracketed paste frame early.
The Stop hook receives a JSON object on stdin at the end of a turn and appends one JSON line to the worker hook file. Observed payload fields include:
| field | meaning |
|---|---|
last_assistant_message |
Full assistant reply text. This is the source of Result.text. |
session_id |
TUI session id passed at process start. |
transcript_path |
Path to the conversation transcript JSONL. |
permission_mode |
Effective permission mode. |
effort |
Effective effort setting. |
hook_event_name |
Hook event name, observed as Stop. |
The transcript JSONL flushes asynchronously and may lag the hook. The worker may
read it for best-effort usage metadata, but reply text must come from
last_assistant_message. If the process is killed immediately after a hook,
transcript metadata can be missing.
A workspace trust dialog can appear before the prompt. The observed dialog is answerable by sending a single carriage return to accept the default.
Authentication failures can surface as TUI startup text followed by process
exit. The worker maps known authentication-failure markers to WorkerAuthError
with the matched marker and drained pty tail, without waiting for the full
readiness timeout.
Mid-session usage-limit or policy text is ordinary assistant output from the
TUI. It is returned as normal Result.text; callers classify that text the same
way they do for stream-json stdout today.
The TUI also renders a weekly-limit usage banner in the pty tail, for example
Youve used N% of your weekly limit. This is useful future signal but is not
parsed today.