fix(statusline): inject plan-usage telemetry via ephemeral CLI flag, 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
This commit is contained in:
timkjr
2026-09-07 19:15:54 -05:00
co-authored by Claude Sonnet 5
parent a164c07f92
commit d4aa3c8cca
9 changed files with 379 additions and 44 deletions
+13 -22
View File
@@ -94,7 +94,6 @@ import {
writeHooksConfig,
updateCaseModel,
stripCaseEnvKeys,
applyStatusLineConfig,
applyAgentSkill,
refreshUserAgentSkill,
seedAgentSessionPreamble,
@@ -948,27 +947,18 @@ export function registerSessionRoutes(
await updateCaseModel(workingDir, body.modelOverride || null);
}
// Plan-usage statusLine exporter (App Settings → Display → "Plan Usage
// Limits"). Claude-only; runs for ANY working dir (linked cases / real repos,
// where most sessions live), mirroring updateCaseModel above.
//
// ADD-ONLY: we never remove on create. Sessions in a repo share one
// settings.local.json, so a single create-with-false (e.g. a client whose
// synced setting hadn't loaded yet) must NOT yank the statusLine out from
// under other live sessions in that repo — that breaks their footer + the
// chip's data feed for everyone. The exporter is benign when the chip is off
// (the footer just shows session status). isOurs-guarded so a user's own
// statusLine is never touched.
//
// Same guard as the hooks call below (499d355): never for a remote attach
// (workingDir is a user@host:session pseudo-path — the mkdir inside
// applyStatusLineConfig would create it as a junk local dir), and only when
// the caller named a workingDir — the process-cwd fallback is $HOME under
// installer-created services, and a statusLine materializing in
// ~/.claude/settings.local.json was never asked for.
if (!remote && body.workingDir && (body.mode ?? 'claude') === 'claude' && body.statusLineTelemetry === true) {
await applyStatusLineConfig(workingDir, true);
}
// Plan-usage telemetry request (App Settings → header chip). NO LONGER a
// disk write here — a settings.local.json 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 and no
// way to undo it (real bug, found 2026-08-31). The request now flows
// through as an ordinary session field (statusLineTelemetryRequested below)
// and Session/TmuxManager resolve it into an EPHEMERAL `claude --settings`
// CLI flag at actual spawn time (resolveStatusLineCliCommand in
// hooks-config.ts) — never written to disk, so a plain `claude` run outside
// Codeman is untouched. That resolution also self-heals: it strips any
// legacy disk-written exporter an older Codeman build left behind.
const statusLineTelemetryRequested = body.statusLineTelemetry === true;
// Hooks for the workspace this session runs in (install vs refresh-only is the
// `workspaceHooksEnabled` setting; see applyWorkspaceHooks). Never for a remote
@@ -1102,6 +1092,7 @@ export function registerSessionRoutes(
resumeSessionId: validatedResumeId,
envOverrides: await clampEnvOverridesForOwner(owner, body.envOverrides),
effort: body.effort,
statusLineTelemetry: statusLineTelemetryRequested,
tmuxHistoryLimit: terminalHistoryConfig.tmuxHistoryLimit,
remote,
owner,