Files
claude-cloak/README.md
Jonas H 1f483ad3d1 Hot-reload onto a rebuilt binary with ctrl-r
Restarting to pick up a code change costs the three things this app
exists to hold: the proxy port, the embedded claude pane, and the live
feed. So ctrl-r does not restart — it execs the binary now on disk into
this same process. execve keeps the pid, the open fds and the child
processes, so the listener socket, the pty and its claude child all
simply carry on; the feed travels in a JSON snapshot.

Build nothing, watch nothing. A build is the user's business, and a
running instance must not decide on its own when to become different
code. Rebuild outside, then press ctrl-r in each instance.

Drain before the exec. It destroys the tokio tasks relaying in-flight
responses, so the proxy stops accepting and finishes what it has first.
The socket stays open throughout (App::listener_fd is a dup), so
requests made during the swap queue in the kernel backlog and are served
by the new image — verified end to end: nothing refused, nothing cut.

Treat the snapshot as advisory. It is written by the old binary and read
by the new one, whose types usually just changed — that is the normal
case, not the edge case. The fd numbers stay in plain fields and the
feed is decoded per session behind a sanitiser, so a schema change costs
the feed and never the port or the pane.
2026-08-27 11:46:15 +02:00

96 lines
3.1 KiB
Markdown

# 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
```sh
cargo build --release
./target/release/claude-cloak
```
Then point Claude Code at the proxy, either per-shell:
```sh
export ANTHROPIC_BASE_URL=http://127.0.0.1:8484
claude
```
or globally in `~/.claude/settings.json`:
```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
| key | action |
|---|---|
| `q` / `Esc` | quit |
| `Tab` / `Shift-Tab` | switch session |
| `j`/`k`, arrows, PgUp/PgDn | scroll (disables follow) |
| `f` / `G` / `End` | follow live tail |
| `g` / `Home` | jump to top |
## 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 **ctrl-r** — without
losing the proxy port, the embedded `claude` pane, or the live feed.
```sh
cargo build # in any terminal, whenever you like
# then press ctrl-r in each running instance
```
claude-cloak never builds anything itself and watches no files. You rebuild the
way you always would; ctrl-r says "run that one now".
ctrl-r 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 `execve`s 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 — ctrl-r 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.