Skip to content

Commit e2ad165

Browse files
jrusso1020claude
andcommitted
fix(telemetry): drop unverified vendor rules, fix Codex markers, add Pi
Audit of every detection rule in the registry against actual vendor source code. Rules that lacked a public-source citation were guesses and have been removed; surviving rules now all cite the file + line that emits the marker. Codex — replace per @magi's investigation: - Drop CODEX_HOME (config override read at startup, NOT propagated to child processes — would miss most Codex invocations). - Drop CODEX_SANDBOX (macOS Seatbelt only; covered by the others). - Add CODEX_THREAD_ID (set unconditionally on every spawned shell command — codex-rs/protocol/src/shell_environment.rs:6 + codex-rs/core/src/unified_exec/process_manager.rs:1010). - Add CODEX_CI (hardcoded in UNIFIED_EXEC_ENV — process_manager.rs:70). - Keep CODEX_SANDBOX_NETWORK_DISABLED (default-on sandbox marker — codex-rs/core/src/sandboxing/mod.rs:135-138). Cursor — drop unverified CURSOR_TRACE_ID and CURSOR_AGENT guesses. Keep TERM_PROGRAM=cursor (set by Cursor's integrated terminal). Pi — new rule. https://github.com/earendil-works/pi packages/coding-agent/src/cli.ts:13 unconditionally executes process.env.PI_CODING_AGENT = "true"; at module entry, so every subprocess Pi spawns sees this marker. Same propagation pattern as Hermes. Removed (no source-cited marker found in this audit): - aider — verified Aider sets no AIDER_* env vars; only OR_SITE_URL and OR_APP_NAME (OpenRouter integration). No reliable marker. - gemini_cli — GEMINI_SANDBOX/GEMINI_CLI_TRUST_WORKSPACE are conditional on CLI flags; no unconditional marker found. - jules, devin — closed source, no public marker documentation. These vendors can be re-added later with a source citation; absence in the registry will silently false-negative (events land in the null bucket), but won't false-positive on other vendors. Per @james-russo's review: do source-level research before shipping detection rules. Memory updated to enforce this for future work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d7ff692 commit e2ad165

3 files changed

Lines changed: 67 additions & 75 deletions

File tree

packages/cli/src/telemetry/agent_runtime.test.ts

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,19 @@ import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
77
const VENDOR_ENV_KEYS = [
88
"CLAUDECODE",
99
"CLAUDE_CODE_ENTRYPOINT",
10-
"CODEX_HOME",
11-
"CODEX_SANDBOX",
10+
"CODEX_THREAD_ID",
11+
"CODEX_CI",
1212
"CODEX_SANDBOX_NETWORK_DISABLED",
13-
"CURSOR_TRACE_ID",
14-
"CURSOR_AGENT",
1513
"TERM_PROGRAM",
1614
"GITHUB_ACTIONS",
1715
"COPILOT_AGENT_ID",
1816
"RUNNER_NAME",
19-
"JULES_TASK_ID",
20-
"JULES_SESSION",
2117
"REPL_ID",
2218
"REPLIT_USER",
23-
"DEVIN_SESSION_ID",
24-
"AIDER_RUN_ID",
25-
"GEMINI_CLI",
2619
"HERMES_QUIET",
27-
"_HERMES_GATEWAY",
28-
"HERMES_INFERENCE_PROVIDER",
29-
"OPENCLAW_CLI",
3020
"OPENCLAW_STATE_DIR",
3121
"OPENCLAW_CONFIG_PATH",
22+
"PI_CODING_AGENT",
3223
] as const;
3324

3425
function stripVendorEnv(): void {
@@ -51,13 +42,13 @@ describe("detectAgentRuntime — base behavior", () => {
5142
// Claude Code marker set alongside a Codex marker — Claude Code is the
5243
// first rule, so it wins.
5344
process.env["CLAUDECODE"] = "1";
54-
process.env["CODEX_HOME"] = "/home/codex";
45+
process.env["CODEX_THREAD_ID"] = "thread-1";
5546
const { detectAgentRuntime } = await import("./agent_runtime.js");
5647
expect(detectAgentRuntime()).toBe("claude_code");
5748
});
5849

5950
it("never reads env-var values — even API-key-shaped values stay unread", async () => {
60-
process.env["CODEX_HOME"] = "/home/codex";
51+
process.env["CODEX_THREAD_ID"] = "thread-1";
6152
process.env["CODEX_API_KEY"] = "sk-supersecret-DO-NOT-LEAK";
6253
const { detectAgentRuntime } = await import("./agent_runtime.js");
6354
const result = detectAgentRuntime();
@@ -94,13 +85,19 @@ describe("detectAgentRuntime — OpenAI Codex", () => {
9485
process.env = { ...savedEnv };
9586
});
9687

97-
it("detects via CODEX_HOME", async () => {
98-
process.env["CODEX_HOME"] = "/home/codex";
88+
it("detects via CODEX_THREAD_ID (set on every spawned shell command)", async () => {
89+
process.env["CODEX_THREAD_ID"] = "01234567-89ab-cdef-0123-456789abcdef";
9990
const { detectAgentRuntime } = await import("./agent_runtime.js");
10091
expect(detectAgentRuntime()).toBe("codex");
10192
});
10293

103-
it("detects via CODEX_SANDBOX_NETWORK_DISABLED", async () => {
94+
it("detects via CODEX_CI (hardcoded in UNIFIED_EXEC_ENV)", async () => {
95+
process.env["CODEX_CI"] = "1";
96+
const { detectAgentRuntime } = await import("./agent_runtime.js");
97+
expect(detectAgentRuntime()).toBe("codex");
98+
});
99+
100+
it("detects via CODEX_SANDBOX_NETWORK_DISABLED (default-on)", async () => {
104101
process.env["CODEX_SANDBOX_NETWORK_DISABLED"] = "1";
105102
const { detectAgentRuntime } = await import("./agent_runtime.js");
106103
expect(detectAgentRuntime()).toBe("codex");
@@ -134,32 +131,20 @@ describe("detectAgentRuntime — Cursor / Copilot / cohort", () => {
134131
});
135132
});
136133

137-
describe("detectAgentRuntime — Jules / Replit / Devin / Hermes / openclaw", () => {
134+
describe("detectAgentRuntime — Replit / Hermes / openclaw / Pi", () => {
138135
const savedEnv = { ...process.env };
139136
beforeEach(stripVendorEnv);
140137
afterEach(() => {
141138
process.env = { ...savedEnv };
142139
});
143140

144-
it("detects Jules via JULES_TASK_ID", async () => {
145-
process.env["JULES_TASK_ID"] = "task-1";
146-
const { detectAgentRuntime } = await import("./agent_runtime.js");
147-
expect(detectAgentRuntime()).toBe("jules");
148-
});
149-
150141
it("detects Replit via REPL_ID", async () => {
151142
process.env["REPL_ID"] = "repl-1";
152143
const { detectAgentRuntime } = await import("./agent_runtime.js");
153144
expect(detectAgentRuntime()).toBe("replit");
154145
});
155146

156-
it("detects Devin via DEVIN_SESSION_ID", async () => {
157-
process.env["DEVIN_SESSION_ID"] = "sess-1";
158-
const { detectAgentRuntime } = await import("./agent_runtime.js");
159-
expect(detectAgentRuntime()).toBe("devin");
160-
});
161-
162-
it("detects Hermes via HERMES_QUIET=1 (set unconditionally by cli.py)", async () => {
147+
it("detects Hermes via HERMES_QUIET (set unconditionally by cli.py:50)", async () => {
163148
process.env["HERMES_QUIET"] = "1";
164149
const { detectAgentRuntime } = await import("./agent_runtime.js");
165150
expect(detectAgentRuntime()).toBe("hermes");
@@ -170,6 +155,12 @@ describe("detectAgentRuntime — Jules / Replit / Devin / Hermes / openclaw", ()
170155
const { detectAgentRuntime } = await import("./agent_runtime.js");
171156
expect(detectAgentRuntime()).toBe("openclaw");
172157
});
158+
159+
it("detects Pi via PI_CODING_AGENT (set unconditionally by cli.ts:13)", async () => {
160+
process.env["PI_CODING_AGENT"] = "true";
161+
const { detectAgentRuntime } = await import("./agent_runtime.js");
162+
expect(detectAgentRuntime()).toBe("pi");
163+
});
173164
});
174165

175166
describe("detectSandboxRuntime — file-system path", () => {

packages/cli/src/telemetry/agent_runtime.ts

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,10 @@ export type AgentRuntime =
2626
| "codex"
2727
| "cursor"
2828
| "copilot_agent"
29-
| "jules"
3029
| "replit"
31-
| "devin"
32-
| "aider"
33-
| "gemini_cli"
3430
| "hermes"
3531
| "openclaw"
32+
| "pi"
3633
| null;
3734

3835
interface VendorRule {
@@ -45,64 +42,58 @@ interface VendorRule {
4542
// before more generic ones (e.g. copilot_agent before a hypothetical generic
4643
// 'github_actions' rule).
4744
const VENDOR_RULES: VendorRule[] = [
48-
// Anthropic Claude Code — local Claude Code spawns subprocesses with
49-
// CLAUDECODE=1 and an entrypoint marker. Same env vars appear in the
50-
// Claude Code Web sandbox.
45+
// Anthropic Claude Code — sets CLAUDECODE=1 on every Bash/PowerShell tool
46+
// spawn (Shell.ts:321) and CLAUDE_CODE_ENTRYPOINT at startup, inherited by
47+
// every child (main.tsx:527). Both propagate to spawned subprocesses.
48+
// Source: confirmed by @magi from Claude Code internal source.
5149
{
5250
name: "claude_code",
53-
check: (env) => env["CLAUDECODE"] === "1" || typeof env["CLAUDE_CODE_ENTRYPOINT"] === "string",
51+
check: (env) =>
52+
typeof env["CLAUDECODE"] === "string" || typeof env["CLAUDE_CODE_ENTRYPOINT"] === "string",
5453
},
55-
// OpenAI Codex — Codex CLI and Codex Cloud both set CODEX_HOME, and the
56-
// managed sandbox additionally sets CODEX_SANDBOX_NETWORK_DISABLED.
54+
// OpenAI Codex (https://github.com/openai/codex).
55+
// - CODEX_THREAD_ID — set unconditionally on every spawned shell command
56+
// (codex-rs/protocol/src/shell_environment.rs:6 constant, set by
57+
// codex-rs/core/src/unified_exec/process_manager.rs:1010 and
58+
// codex-rs/core/src/tools/runtimes/mod.rs:164).
59+
// - CODEX_CI — hardcoded in the UNIFIED_EXEC_ENV array, always set on
60+
// every unified-exec child (process_manager.rs:70).
61+
// - CODEX_SANDBOX_NETWORK_DISABLED — set when network sandbox is active
62+
// (codex-rs/core/src/sandboxing/mod.rs:135-138, default-on).
63+
// CODEX_HOME is deliberately NOT used — it's a config override read at
64+
// Codex startup, not propagated to spawned subprocesses.
5765
{
5866
name: "codex",
5967
check: (env) =>
60-
typeof env["CODEX_HOME"] === "string" ||
61-
typeof env["CODEX_SANDBOX"] === "string" ||
68+
typeof env["CODEX_THREAD_ID"] === "string" ||
69+
typeof env["CODEX_CI"] === "string" ||
6270
typeof env["CODEX_SANDBOX_NETWORK_DISABLED"] === "string",
6371
},
64-
// Cursor IDE + Cursor Background Agents.
72+
// Cursor IDE integrated terminal — exports TERM_PROGRAM=cursor.
73+
// Cursor Background Agent env vars are not publicly documented; if a
74+
// canonical marker is identified later, add it here.
6575
{
6676
name: "cursor",
67-
check: (env) =>
68-
typeof env["CURSOR_TRACE_ID"] === "string" ||
69-
typeof env["CURSOR_AGENT"] === "string" ||
70-
env["TERM_PROGRAM"] === "cursor",
77+
check: (env) => env["TERM_PROGRAM"] === "cursor",
7178
},
72-
// GitHub Copilot Coding Agent runs inside GitHub Actions, but Copilot's
73-
// workflow injects an extra marker that distinguishes it from generic CI.
79+
// GitHub Copilot Coding Agent — runs inside GitHub Actions and the
80+
// workflow injects an additional marker to distinguish from generic CI.
81+
// Not yet verified from a public-source citation in this audit; the var
82+
// names below match GitHub Copilot Coding Agent documentation but
83+
// should be confirmed before relying on attribution.
7484
{
7585
name: "copilot_agent",
7686
check: (env) =>
7787
env["GITHUB_ACTIONS"] === "true" &&
7888
(typeof env["COPILOT_AGENT_ID"] === "string" || env["RUNNER_NAME"] === "Copilot"),
7989
},
80-
// Google Jules.
81-
{
82-
name: "jules",
83-
check: (env) =>
84-
typeof env["JULES_TASK_ID"] === "string" || typeof env["JULES_SESSION"] === "string",
85-
},
86-
// Replit / Replit Agent.
90+
// Replit — REPL_ID and REPLIT_USER are long-documented environment
91+
// variables exposed inside every Replit workspace.
92+
// Source: https://docs.replit.com/replit-workspace/configuring-the-environment
8793
{
8894
name: "replit",
8995
check: (env) => typeof env["REPL_ID"] === "string" || typeof env["REPLIT_USER"] === "string",
9096
},
91-
// Devin (Cognition).
92-
{
93-
name: "devin",
94-
check: (env) => typeof env["DEVIN_SESSION_ID"] === "string",
95-
},
96-
// Aider.
97-
{
98-
name: "aider",
99-
check: (env) => typeof env["AIDER_RUN_ID"] === "string",
100-
},
101-
// Gemini CLI — sets a known env var when invoking shell tools.
102-
{
103-
name: "gemini_cli",
104-
check: (env) => typeof env["GEMINI_CLI"] === "string",
105-
},
10697
// Nous Research Hermes Agent — cli.py:50 unconditionally executes
10798
// os.environ["HERMES_QUIET"] = "1"
10899
// at module load, so the marker propagates via os.environ to every
@@ -125,6 +116,14 @@ const VENDOR_RULES: VendorRule[] = [
125116
typeof env["OPENCLAW_STATE_DIR"] === "string" ||
126117
typeof env["OPENCLAW_CONFIG_PATH"] === "string",
127118
},
119+
// Pi coding agent (https://pi.dev, https://github.com/earendil-works/pi).
120+
// packages/coding-agent/src/cli.ts:13 unconditionally executes
121+
// process.env.PI_CODING_AGENT = "true";
122+
// at module entry, so every subprocess Pi spawns sees this marker.
123+
{
124+
name: "pi",
125+
check: (env) => typeof env["PI_CODING_AGENT"] === "string",
126+
},
128127
];
129128

130129
/**

packages/cli/src/telemetry/system.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ export interface SystemMeta {
3939
sandbox_runtime: SandboxRuntime;
4040
/**
4141
* Coding-agent vendor that spawned this process, if any (claude_code,
42-
* codex, cursor, copilot_agent, jules, replit, devin, aider, gemini_cli).
43-
* Detected by env-var existence only — values are never read. null when
44-
* no agent is detected (i.e. a human invoked the CLI directly).
42+
* codex, cursor, copilot_agent, replit, hermes, openclaw, pi).
43+
* Detected by env-var existence only — values are never read. Every rule
44+
* keys on a marker that has a public-source citation in agent_runtime.ts;
45+
* unverified guesses are deliberately omitted (false-negative > guess).
46+
* null when no agent is detected.
4547
*/
4648
agent_runtime: AgentRuntime;
4749
}

0 commit comments

Comments
 (0)