The keybindings had no red thread because there were three focus states and every key had to ask "is this mine or the child's" — hence ctrl-q, because plain q was forwarded. Replace the whole model: unprefixed keys belong to the embedded claude, always, and everything cloak owns sits behind ctrl-space (CT_PREFIX to change), tmux-style, in one table that the which-key popup, the footer hint and the dispatch all read. Esc is the only key with a rule of its own, and it is about reachability rather than modes: it closes the topmost overlay, and with nothing open it goes to the child, so interrupt and Esc-Esc rewind keep working. View state — filters, the lane the feed shows — is a setting, not a mode, and is deliberately not escapable. That removes the focus model entirely, which lets the two feeds collapse into one: the feed renders whichever lane feed_lane names, at full width, and the stream picker chooses it. Subagents used to live in a modal popup holding a second draw_feed with its own cache and scroll model, rendering the same thing twice. The sessions panel loses its half of the screen the same way. Both pickers become bottom strips with the feed readable above them, so walking the list previews each row — which is the view-without-resuming that /resume cannot do, and frees enter to attach the pane. Adds a find bar with in-place highlighting, scrolling to the matching line rather than the containing entry, and drops the two keys that served the old layout.
4.9 KiB
claude-cloak
A TUI that displays Claude Code's streams token-by-token — thinking, text, and tool calls — by sitting as a pass-through proxy between Claude Code and the Anthropic API.
No extra usage: it never issues requests of its own; it observes the SSE stream of requests Claude Code was already making, forwarding bytes verbatim and unbuffered.
claude ──ANTHROPIC_BASE_URL──▶ claude-cloak (127.0.0.1:8484) ──▶ api.anthropic.com
│
▼
TUI: live token feed
Usage
cargo build --release
./target/release/claude-cloak
Then point Claude Code at the proxy, either per-shell:
export ANTHROPIC_BASE_URL=http://127.0.0.1:8484
claude
or globally in ~/.claude/settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:8484" } }
Note: with the global setting, Claude Code can't reach the API while the proxy isn't running.
Keys
Everything you type goes to the embedded claude. There is no focus model and
no key you have to think about — q, j and Esc all reach Claude Code,
because the app's own keys live behind a prefix, tmux-style.
Press ctrl-space and a which-key popup shows what is available. The keys work immediately; you never have to wait for the popup.
^space + |
action |
|---|---|
s |
sessions — j/k previews each one in the feed, enter opens it in claude, Esc keeps reading it. Turn tree with space, branch with b |
a |
streams — j/k previews the main chain, each subagent, each hosted tool call. Sized to the list, capped at a third |
n |
new session (model picker) |
c |
continue the most recent session |
f |
filter which entry kinds show |
/ |
find bar — matches highlight in place, enter/↓ next, ↑ prev, with a 3/12 counter |
] [ |
next / previous user prompt |
. |
back to the live main chain, tailing it — undoes a picked session, a picked lane, and a parked scroll |
z Z |
zoom the pane / zoom the feed |
r |
hot reload |
q |
quit |
Repeatable keys (] [) keep the popup open, so ^space ]]] walks.
^space ^space sends a literal prefix to the child.
Two keys are the app's without a prefix, for the same reason a terminal keeps them for itself: the wheel and shift+PgUp/PgDn scroll the feed (or the pane's own scrollback while the pane is zoomed). They keep working while a list is open, so you can read what you highlighted before committing to it.
Esc closes the topmost overlay. With nothing open it goes to Claude Code, so interrupt and Esc-Esc rewind keep working. Filters and the stream you picked are settings, not modes — Esc never resets them.
CT_PREFIX changes the prefix (CT_PREFIX=ctrl-b, ctrl-], f1, …), for the
case where your Claude Code wants ctrl-space for itself. CT_DEBUG_KEYS=1 shows
what your terminal actually delivers. ctrl-q quits from anywhere, as a way out
if the prefix never arrives.
Display
- ✻ thinking — dim italic, streamed token-by-token
- text — rendered as markdown (
tui-markdown) - ⚙ tool calls — input JSON pretty-printed on completion, raw fragments while streaming
- meta — message boundaries with model, context size, stop reason, token counts
- errors — API/stream errors in red
Sessions are keyed by the Claude Code session ID found in request metadata; concurrent requests (subagents) tap independently.
--headless runs the proxy without the TUI. CT_PORT overrides the port (default 8484).
Hot reload
Swap a running instance onto a newly built binary with ^space r — without
losing the proxy port, the embedded claude pane, or the live feed.
cargo build # in any terminal, whenever you like
# then press ^space r in each running instance
claude-cloak never builds anything itself and watches no files. You rebuild the way you always would; the key says "run that one now".
It execs the same path the instance was started from, so a debug instance reloads onto a rebuilt debug binary and a release instance onto a rebuilt release one. It does not cross profiles.
How it survives
The app execves the new binary into its own process, so the pid, the open
file descriptors and the child processes all stay. Before the exec the proxy
drains: it stops accepting and lets in-flight responses finish, while the
listening socket stays open so requests made during the swap wait in the kernel
backlog and are served by the new code. Nothing is refused and nothing is
truncated.
The footer shows ⟳ reloading… while it drains, then · reload #1 once the new
code is running. A failed exec — pressed while the linker still had the file
open — reports ⚠ reload failed: … and changes nothing; press it again.
A state snapshot the new types no longer fit costs the feed only — never the port and never the pane.