fix(statusline): guard the shim for Docker, drop the ancestor walk, remove on chip-off (#405)

Follow-ups to the delegating statusline shim from discussion #405, answering
the four design questions and the Docker one raised there.

Docker cases: the injected command is now a self-selecting shell guard,
`if [ -x <node> ] && [ -f <shim> ]; then exec <node> <shim>; fi;` followed by
the inline curl exporter. A Docker case bind-mounts the workspace, and with it
settings.local.json, at the same absolute path inside the container, but
neither the host's node nor ~/.codeman exists there, so a bare shim command
would have rendered a broken statusline in every container session. The same
string now runs the shim on the host and the curl inside the container. The
settings-save injection loop needs no docker guard for that reason; it does
skip remote attaches now, whose workingDir is a user@host pseudo-path.

Settings precedence: the shim reads exactly the three files Claude Code
documents, .claude/settings.local.json and .claude/settings.json under
workspace.project_dir (the launch directory), then ~/.claude/settings.json.
No ancestor walk and no user-level settings.local.json: delegating to a
command Claude Code would have ignored is the original failure in a new coat.

The bare word: all three paths that produced `codeman` are gone. The route
answers an unknown session with an empty body, formatSessionStatusText()
returns '' with nothing to show, and the inline fallback ends in `|| true`
(plus `curl -f`, so an HTTP error body never renders as the statusline). The
shim also treats a literal `codeman` from an older server as no telemetry and
prints nothing rather than a brand word when it has neither a delegate nor a
footer, which is what Claude Code shows a user with no statusline of their own.

Removal: turning the plan-usage chip OFF now takes the exporter out of the
workspaces of the caller's live Claude sessions. statusLineTelemetry:false is
sent only by the save that flips the chip off on that device
(statusLineTelemetryAction() in settings-ui.js), so a phone whose chip was
never on cannot strip the exporter a desktop's chip depends on; a second
device with the chip still on re-injects on its next save or session create.
Nothing in src/ called applyStatusLineConfig(dir, false) before.

Tests run the generated shim AND the injected command as real subprocesses
(the fallback half with the shim path pointed at nothing, the container's
view), plus both directions of the action field through PUT /api/settings.
Measured against the live server: a render costs ~85 ms through the shim
versus ~26 ms for the old inline curl (node start ~33 ms, the rest TLS to
the loopback HTTPS server plus the delegate spawn), off the input path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-09-14 13:45:32 +02:00
parent c375268879
commit d1cd7884d4
17 changed files with 623 additions and 218 deletions
+20 -18
View File
@@ -2,25 +2,27 @@
"aicodeman": patch
---
feat(statusline): stop the plan-usage exporter from stealing the user's statusline
fix(statusline): stop the plan-usage exporter from stealing the user's statusline (#405)
Claude Code ranks a repo's `.claude/settings.local.json` above `~/.claude/settings.json`, so
the statusLine Codeman injects for the Plan Usage chip SHADOWS whatever statusline the user
configured globally. The inline exporter then printed Codeman's own footer in its place, and
running `claude` by hand in a managed repo rendered the bare word `codeman` — the response
the server returns for a session id it does not know.
the statusLine Codeman injects for the Plan Usage chip shadowed whatever statusline the user
had configured globally, and running `claude` by hand in a managed repo rendered the bare word
`codeman`. The exporter is now a generated, delegating shim (`src/statusline-shim.ts`, the
`deepseek-status-shim` pattern): it forwards the same payload to `/api/status-telemetry` and,
concurrently, runs the statusline it shadows and prints that. Codeman's footer appears only when
there is nothing to shadow, and with neither the line stays blank. The delegate is resolved at
render time from the three settings files Claude Code documents (`workspace.project_dir` first,
then `~/.claude/settings.json`), never from an ancestor directory or a user-level
`settings.local.json`.
The exporter is now a generated shim, `src/statusline-shim.ts`, following the
`deepseek-status-shim` pattern: versioned `.mjs` written into the data dir, refreshed on a
marker change, temp-and-rename so a live render cannot read a half-written file. It forwards
the same payload to `/api/status-telemetry` and, concurrently, resolves the statusline it is
shadowing and prints that instead. Codeman's footer still appears when there is nothing to
shadow, so the exporter keeps its value on a machine with no statusline of its own.
The injected command is a self-selecting shell guard that runs the shim where it exists and
falls through to the inline curl exporter where it does not, so the same bind-mounted
`settings.local.json` still reports telemetry from inside a Docker case's container. Ownership
accepts both the new `codeman-statusline-shim` token and the old `/api/status-telemetry`
command, so repos managed by an older Codeman upgrade in place. `POST /api/status-telemetry`
answers an unknown session with an empty body instead of `codeman`, and the session-status
footer is empty rather than a brand word when the payload carries nothing to show.
The delegate is resolved at render time by walking the settings files Claude Code consults,
nearest first, skipping Codeman's own entry in either the shim or the pre-shim form. Late
resolution means editing a global statusline takes effect with no reinjection. Ownership now
keys on the version-free `codeman-statusline-shim` token, and `applyStatusLineConfig` still
reads the old `/api/status-telemetry` command as ours so managed repos upgrade in place
rather than being mistaken for hand-authored. A hand-authored statusLine is left alone
exactly as before.
Turning the Plan Usage chip off now removes the exporter from the workspaces of your live Claude
sessions. The removal rides only the settings save that flips the chip off on a device, so a
phone whose chip was never on cannot strip the exporter a desktop depends on.