Skip to content

fix(statusline): inject plan-usage telemetry via ephemeral CLI flag, never disk - #361

Open
timkjr wants to merge 2 commits into
Ark0N:masterfrom
timkjr:fix/statusline-injection-opt-out
Open

fix(statusline): inject plan-usage telemetry via ephemeral CLI flag, never disk#361
timkjr wants to merge 2 commits into
Ark0N:masterfrom
timkjr:fix/statusline-injection-opt-out

Conversation

@timkjr

Copy link
Copy Markdown

Summary

The plan-usage chip's statusLine exporter was written into the case's .claude/settings.local.json. That file-based statusLine took precedence over the user's own global/project statusline for anyclaude invocation in that directory — including entirely outside Codeman — with no disclosure in the App Settings UI (labeled only as a header-display toggle) and no way to remove it once written (the removal code path was dead code: nothing ever called applyStatusLineConfig(path, false)).

Fixes this by injecting the exporter as an ephemeral claude --settings '{"statusLine":{...}}' CLI flag, resolved fresh at spawn time and merged with effort/ultracode into one --settings object (Claude Code accepts only one --settings flag per invocation). Never touches disk, so a plain claude run outside Codeman is completely untouched. Self-healing: a legacy disk-written exporter from an older build is stripped the first time a session starts in that workspace again. Still respects a user's own hand-authored statusline — it's never overridden.

Bonus: since the exporter no longer lives in a fixed file, it can now wrap the user's own real statusline instead of just backing off. findEffectiveUserStatusLineCommand() walks Claude Code's actual settings precedence (project settings.local.json → project settings.json → user ~/.claude/settings.json) to find whatever's really configured, and the shared exporter script fires the telemetry POST in a background subshell (confirmed non-blocking, ~4ms, even against an unreachable endpoint) then execs the user's real command with the same stdin — so the user keeps seeing their own statusline, and the plan-usage chip still gets its data.

A subtler bug found mid-fix

The exporter's command legitimately depends on $CODEMAN_SESSION_ID / $CODEMAN_API_URL / $CODEMAN_HOOK_SECRET_FILE and an internal $INPUT, all meant to be expanded only when Claude Code itself executes the statusline, using the pane's tmux setenv'd environment. An early version of this fix passed that command as literal text through --settings, which routed it through execSync's own implicit /bin/sh -c (tmux respawn-pane's bash -c "..." wrapper) before Claude Code ever saw it — POSIX double quotes don't suppress $ expansion, so those vars got expanded prematurely against the server process's own environment (unset there), producing malformed JSON that printed as literal error text in the statusline.

Fixed by writing the exporter as a real, shared script file (ensureStatusLineExporterScript, marker-versioned so stale copies self-heal) and passing only its bare path via --settings — a path has no shell metacharacters for any intermediate shell layer to mangle.

Test plan

  • npm test (full CI gate) green throughout
  • New unit tests: test/hooks-config.test.ts (resolveStatusLineCliCommand, findEffectiveUserStatusLineCommand), test/statusline-cli-flag.test.ts (real-shell-execution based, not hand-rolled unescaping)
  • Verified live against a real Claude CLI on an isolated tmux socket (outside the app, own statusline shown untouched; inside the app, telemetry flows)
  • Verified via direct execSync reproduction of the exact nested shell wrapping createSession/respawnPane use, with an arg-echoing stand-in for claude, before trusting the fix
  • Confirmed the background telemetry POST never blocks statusline rendering (timed against an unreachable endpoint: ~4ms)
  • Deployed to my own production instance; confirmed both in-app and outside-app behavior, including a remote SSH session (unaffected either way — remote sessions were never in scope for this injection)

timkjrand others added 2 commits August 30, 2026 17:31
…never disk
Codeman's plan-usage chip wrote a statusLine.command into the case's
.claude/settings.local.json to receive Claude Code's rate_limits blob.
That file-based statusLine took precedence over the user's own
global/project statusline for ANY `claude` run in that directory,
including entirely outside Codeman, with no disclosure in the App
Settings UI (labeled only as a header-display toggle) and no way to
remove it once written (the removal code path was unreachable dead
code — nothing ever called it with false).
Replace the disk write with an EPHEMERAL `claude --settings
'{"statusLine":{...}}'` CLI flag, resolved fresh at spawn time
(resolveStatusLineCliCommand in hooks-config.ts) and merged with
effort/ultracode into one --settings object (buildClaudeSettingsFlag
in tmux-manager.ts, since Claude Code accepts only one --settings
flag). Never touches disk, so a plain `claude` run outside Codeman is
untouched. Self-healing: any legacy disk-written exporter from an
older build is stripped the first time a session starts in that
workspace again. Still respects a user's own hand-authored statusLine
(skips the flag entirely rather than overriding it).
Mid-fix bug found and fixed: the exporter's command legitimately
depends on $CODEMAN_SESSION_ID/$CODEMAN_API_URL/$CODEMAN_HOOK_SECRET_FILE
and an internal $INPUT, all meant to be expanded only when Claude Code
itself executes the statusline, using the pane's tmux-setenv'd
environment. Passing that text through --settings routed it through
execSync's own implicit /bin/sh -c first (tmux respawn-pane's
`bash -c "..."` wrapper) — POSIX double quotes don't suppress $
expansion, so those vars got expanded prematurely against the
server's own environment (unset there), producing malformed JSON that
printed as literal error text in the statusline. Fixed by writing the
exporter as a real, shared script file (ensureStatusLineExporterScript,
marker-versioned so stale copies self-heal) and passing only its bare
path via --settings — nothing for any intermediate shell to mangle.
Verified against a real Claude CLI on an isolated tmux socket, and via
direct execSync reproduction of the exact nested wrapping
createSession/respawnPane use.
A hard "never inject, even ephemerally" kill-switch was added and then
removed in the same pass: with the disk-leak fixed, disabling
injection only cost the plan-usage telemetry the feature exists to
provide, for no remaining benefit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GyMnFWnUzc41TDeHg9juW
…ping it
Now that the exporter no longer lives in a fixed per-case file, it can
compose with the user's actual configured statusline rather than just
backing off when one is found.
findEffectiveUserStatusLineCommand() walks Claude Code's own settings
precedence for a workspace: project-local .claude/settings.local.json
> project-shared .claude/settings.json > the user's global
~/.claude/settings.json. A legacy Codeman-marked entry left behind in
the project's own settings.local.json is never treated as a real user
command — it's skipped and precedence continues to the next layer.
The shared exporter script (bumped to a V2 marker so stale copies
self-heal) now fires the telemetry POST in a background subshell —
its own stdout/stderr discarded so nothing leaks into the visible
statusline, and confirmed non-blocking (~4ms, even against an
unreachable endpoint) — then, if the pane's environment carries
CODEMAN_USER_STATUSLINE_CMD, feeds it the same stdin blob and relays
its stdout as ours. Otherwise it falls back to the plain "codeman"
marker as before.
The discovered command is threaded to the pane via `tmux setenv
CODEMAN_USER_STATUSLINE_CMD` (_configureStatusLineUserCommand) rather
than embedded in the spawn command line, for the same
premature-shell-expansion reason as the parent commit: tmux stores a
setenv value verbatim and never re-parses it, so once shellescape()d
for that one command, the command's own $/quotes survive untouched
into the pane's environment.
Verified live via direct shell execution of the generated script
(both branches: fallback and user-command wrapping) before deploy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GyMnFWnUzc41TDeHg9juW
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@timkjr