fix(statusline): inject plan-usage telemetry via ephemeral CLI flag, never disk - #361
Open
timkjr wants to merge 2 commits into
Open
fix(statusline): inject plan-usage telemetry via ephemeral CLI flag, never disk#361timkjr wants to merge 2 commits into
timkjr wants to merge 2 commits into
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The plan-usage chip's statusLine exporter was written into the case's
.claude/settings.local.json. That file-basedstatusLinetook precedence over the user's own global/project statusline for anyclaudeinvocation 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 calledapplyStatusLineConfig(path, false)).Fixes this by injecting the exporter as an ephemeral
claude --settings '{"statusLine":{...}}'CLI flag, resolved fresh at spawn time and merged witheffort/ultracodeinto one--settingsobject (Claude Code accepts only one--settingsflag per invocation). Never touches disk, so a plainclauderun 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 (projectsettings.local.json→ projectsettings.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_FILEand an internal$INPUT, all meant to be expanded only when Claude Code itself executes the statusline, using the pane'stmux setenv'd environment. An early version of this fix passed that command as literal text through--settings, which routed it throughexecSync's own implicit/bin/sh -c(tmuxrespawn-pane'sbash -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 throughouttest/hooks-config.test.ts(resolveStatusLineCliCommand,findEffectiveUserStatusLineCommand),test/statusline-cli-flag.test.ts(real-shell-execution based, not hand-rolled unescaping)execSyncreproduction of the exact nested shell wrappingcreateSession/respawnPaneuse, with an arg-echoing stand-in forclaude, before trusting the fix