mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
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:
co-authored by
Claude Sonnet 5
parent
a164c07f92
commit
d4aa3c8cca
@@ -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,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { getCli } from '../../config/cli-registry/registry.js';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
||||
@@ -33,7 +32,6 @@ import {
|
||||
import { subagentWatcher } from '../../subagent-watcher.js';
|
||||
import { imageWatcher } from '../../image-watcher.js';
|
||||
import { workflowRunWatcher } from '../../workflow-run-watcher.js';
|
||||
import { applyStatusLineConfig } from '../../hooks-config.js';
|
||||
import { getLifecycleLog } from '../../session-lifecycle-log.js';
|
||||
import {
|
||||
buildAwayDigest,
|
||||
@@ -994,7 +992,9 @@ export function registerSystemRoutes(
|
||||
}
|
||||
// statusLineTelemetry and acknowledgeUnauthTunnel are ACTION fields (not stored
|
||||
// settings) — strip them before persisting so settings.json stays clean.
|
||||
const { statusLineTelemetry, acknowledgeUnauthTunnel, ...settingsToStore } = settings;
|
||||
// statusLineTelemetry is an action field only (see below); it must never
|
||||
// land in settingsToStore.
|
||||
const { statusLineTelemetry: _statusLineTelemetry, acknowledgeUnauthTunnel, ...settingsToStore } = settings;
|
||||
const merged = { ...existing, ...settingsToStore };
|
||||
await fs.writeFile(SETTINGS_PATH, JSON.stringify(merged, null, 2));
|
||||
|
||||
@@ -1034,21 +1034,18 @@ export function registerSystemRoutes(
|
||||
});
|
||||
|
||||
// Plan-usage chip: its DISPLAY is per-device (client-side, see settings-ui.js).
|
||||
// Telemetry COLLECTION is server-side and enable-sticky — when a client turns
|
||||
// the chip ON it sends statusLineTelemetry:true and we (re)inject our exporter
|
||||
// into every ACTIVE Claude session's working dir so the live % starts flowing
|
||||
// immediately (no new session needed). We deliberately never auto-REMOVE here:
|
||||
// the exporter is benign/print-through and a per-repo settings.local.json is
|
||||
// shared by sibling sessions, so one device's "off" must not yank the exporter
|
||||
// another device's chip depends on. Each dir handled once.
|
||||
if (statusLineTelemetry === true) {
|
||||
const dirs = new Set<string>();
|
||||
for (const session of ctx.sessions.values()) {
|
||||
if (getCli(session.mode)?.capabilities.statusLineTelemetry && session.workingDir)
|
||||
dirs.add(session.workingDir);
|
||||
}
|
||||
await Promise.all([...dirs].map((dir) => applyStatusLineConfig(dir, true).catch(() => {})));
|
||||
}
|
||||
// Telemetry COLLECTION was previously server-side and enable-sticky here —
|
||||
// toggling the chip ON re-injected a statusLine.command into every ACTIVE
|
||||
// Claude session's settings.local.json so live % started flowing without a
|
||||
// new session. That disk write is exactly the bug fixed 2026-08-31 (it took
|
||||
// precedence over the user's own statusline for ANY `claude` run in that
|
||||
// directory, including outside Codeman, with no way to undo it). Telemetry
|
||||
// is now requested per-session at CREATE/RESPAWN time only (statusLineTelemetry
|
||||
// threaded through cron/ralph-loop/quick-start/interactive-create, see those
|
||||
// route handlers), resolved into an ephemeral `--settings` CLI flag — fixed at
|
||||
// spawn, so there is nothing to (re)inject into an ALREADY-RUNNING session
|
||||
// here, unlike the old disk mechanism. The action field above is received and
|
||||
// discarded; flipping the chip ON only affects sessions created from now on.
|
||||
|
||||
// Handle tunnel toggle dynamically
|
||||
if ('tunnelEnabled' in settings) {
|
||||
|
||||
Reference in New Issue
Block a user