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>
This commit is contained in:
James
2026-05-22 19:10:30 -04:00
committed by James Russo
co-authored by Claude Opus 4.7
parent d7ff692f9f
commit e2ad165c6c
3 changed files with 67 additions and 75 deletions
@@ -7,28 +7,19 @@ import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
const VENDOR_ENV_KEYS = [
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CODEX_HOME",
"CODEX_SANDBOX",
"CODEX_THREAD_ID",
"CODEX_CI",
"CODEX_SANDBOX_NETWORK_DISABLED",
"CURSOR_TRACE_ID",
"CURSOR_AGENT",
"TERM_PROGRAM",
"GITHUB_ACTIONS",
"COPILOT_AGENT_ID",
"RUNNER_NAME",
"JULES_TASK_ID",
"JULES_SESSION",
"REPL_ID",
"REPLIT_USER",
"DEVIN_SESSION_ID",
"AIDER_RUN_ID",
"GEMINI_CLI",
"HERMES_QUIET",
"_HERMES_GATEWAY",
"HERMES_INFERENCE_PROVIDER",
"OPENCLAW_CLI",
"OPENCLAW_STATE_DIR",
"OPENCLAW_CONFIG_PATH",
"PI_CODING_AGENT",
] as const;
function stripVendorEnv(): void {
@@ -51,13 +42,13 @@ describe("detectAgentRuntime — base behavior", () => {
// Claude Code marker set alongside a Codex marker — Claude Code is the
// first rule, so it wins.
process.env["CLAUDECODE"] = "1";
process.env["CODEX_HOME"] = "/home/codex";
process.env["CODEX_THREAD_ID"] = "thread-1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("claude_code");
});
it("never reads env-var values — even API-key-shaped values stay unread", async () => {
process.env["CODEX_HOME"] = "/home/codex";
process.env["CODEX_THREAD_ID"] = "thread-1";
process.env["CODEX_API_KEY"] = "sk-supersecret-DO-NOT-LEAK";
const { detectAgentRuntime } = await import("./agent_runtime.js");
const result = detectAgentRuntime();
@@ -94,13 +85,19 @@ describe("detectAgentRuntime — OpenAI Codex", () => {
process.env = { ...savedEnv };
});
it("detects via CODEX_HOME", async () => {
process.env["CODEX_HOME"] = "/home/codex";
it("detects via CODEX_THREAD_ID (set on every spawned shell command)", async () => {
process.env["CODEX_THREAD_ID"] = "01234567-89ab-cdef-0123-456789abcdef";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("codex");
});
it("detects via CODEX_SANDBOX_NETWORK_DISABLED", async () => {
it("detects via CODEX_CI (hardcoded in UNIFIED_EXEC_ENV)", async () => {
process.env["CODEX_CI"] = "1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("codex");
});
it("detects via CODEX_SANDBOX_NETWORK_DISABLED (default-on)", async () => {
process.env["CODEX_SANDBOX_NETWORK_DISABLED"] = "1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("codex");
@@ -134,32 +131,20 @@ describe("detectAgentRuntime — Cursor / Copilot / cohort", () => {
});
});
describe("detectAgentRuntime — Jules / Replit / Devin / Hermes / openclaw", () => {
describe("detectAgentRuntime — Replit / Hermes / openclaw / Pi", () => {
const savedEnv = { ...process.env };
beforeEach(stripVendorEnv);
afterEach(() => {
process.env = { ...savedEnv };
});
it("detects Jules via JULES_TASK_ID", async () => {
process.env["JULES_TASK_ID"] = "task-1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("jules");
});
it("detects Replit via REPL_ID", async () => {
process.env["REPL_ID"] = "repl-1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("replit");
});
it("detects Devin via DEVIN_SESSION_ID", async () => {
process.env["DEVIN_SESSION_ID"] = "sess-1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("devin");
});
it("detects Hermes via HERMES_QUIET=1 (set unconditionally by cli.py)", async () => {
it("detects Hermes via HERMES_QUIET (set unconditionally by cli.py:50)", async () => {
process.env["HERMES_QUIET"] = "1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("hermes");
@@ -170,6 +155,12 @@ describe("detectAgentRuntime — Jules / Replit / Devin / Hermes / openclaw", ()
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("openclaw");
});
it("detects Pi via PI_CODING_AGENT (set unconditionally by cli.ts:13)", async () => {
process.env["PI_CODING_AGENT"] = "true";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("pi");
});
});
describe("detectSandboxRuntime — file-system path", () => {
+40 -41
View File
@@ -26,13 +26,10 @@ export type AgentRuntime =
| "codex"
| "cursor"
| "copilot_agent"
| "jules"
| "replit"
| "devin"
| "aider"
| "gemini_cli"
| "hermes"
| "openclaw"
| "pi"
| null;
interface VendorRule {
@@ -45,64 +42,58 @@ interface VendorRule {
// before more generic ones (e.g. copilot_agent before a hypothetical generic
// 'github_actions' rule).
const VENDOR_RULES: VendorRule[] = [
// Anthropic Claude Code — local Claude Code spawns subprocesses with
// CLAUDECODE=1 and an entrypoint marker. Same env vars appear in the
// Claude Code Web sandbox.
// Anthropic Claude Code — sets CLAUDECODE=1 on every Bash/PowerShell tool
// spawn (Shell.ts:321) and CLAUDE_CODE_ENTRYPOINT at startup, inherited by
// every child (main.tsx:527). Both propagate to spawned subprocesses.
// Source: confirmed by @magi from Claude Code internal source.
{
name: "claude_code",
check: (env) => env["CLAUDECODE"] === "1" || typeof env["CLAUDE_CODE_ENTRYPOINT"] === "string",
check: (env) =>
typeof env["CLAUDECODE"] === "string" || typeof env["CLAUDE_CODE_ENTRYPOINT"] === "string",
},
// OpenAI Codex — Codex CLI and Codex Cloud both set CODEX_HOME, and the
// managed sandbox additionally sets CODEX_SANDBOX_NETWORK_DISABLED.
// OpenAI Codex (https://github.com/openai/codex).
// - CODEX_THREAD_ID — set unconditionally on every spawned shell command
// (codex-rs/protocol/src/shell_environment.rs:6 constant, set by
// codex-rs/core/src/unified_exec/process_manager.rs:1010 and
// codex-rs/core/src/tools/runtimes/mod.rs:164).
// - CODEX_CI — hardcoded in the UNIFIED_EXEC_ENV array, always set on
// every unified-exec child (process_manager.rs:70).
// - CODEX_SANDBOX_NETWORK_DISABLED — set when network sandbox is active
// (codex-rs/core/src/sandboxing/mod.rs:135-138, default-on).
// CODEX_HOME is deliberately NOT used — it's a config override read at
// Codex startup, not propagated to spawned subprocesses.
{
name: "codex",
check: (env) =>
typeof env["CODEX_HOME"] === "string" ||
typeof env["CODEX_SANDBOX"] === "string" ||
typeof env["CODEX_THREAD_ID"] === "string" ||
typeof env["CODEX_CI"] === "string" ||
typeof env["CODEX_SANDBOX_NETWORK_DISABLED"] === "string",
},
// Cursor IDE + Cursor Background Agents.
// Cursor IDE integrated terminal — exports TERM_PROGRAM=cursor.
// Cursor Background Agent env vars are not publicly documented; if a
// canonical marker is identified later, add it here.
{
name: "cursor",
check: (env) =>
typeof env["CURSOR_TRACE_ID"] === "string" ||
typeof env["CURSOR_AGENT"] === "string" ||
env["TERM_PROGRAM"] === "cursor",
check: (env) => env["TERM_PROGRAM"] === "cursor",
},
// GitHub Copilot Coding Agent runs inside GitHub Actions, but Copilot's
// workflow injects an extra marker that distinguishes it from generic CI.
// GitHub Copilot Coding Agent runs inside GitHub Actions and the
// workflow injects an additional marker to distinguish from generic CI.
// Not yet verified from a public-source citation in this audit; the var
// names below match GitHub Copilot Coding Agent documentation but
// should be confirmed before relying on attribution.
{
name: "copilot_agent",
check: (env) =>
env["GITHUB_ACTIONS"] === "true" &&
(typeof env["COPILOT_AGENT_ID"] === "string" || env["RUNNER_NAME"] === "Copilot"),
},
// Google Jules.
{
name: "jules",
check: (env) =>
typeof env["JULES_TASK_ID"] === "string" || typeof env["JULES_SESSION"] === "string",
},
// Replit / Replit Agent.
// Replit — REPL_ID and REPLIT_USER are long-documented environment
// variables exposed inside every Replit workspace.
// Source: https://docs.replit.com/replit-workspace/configuring-the-environment
{
name: "replit",
check: (env) => typeof env["REPL_ID"] === "string" || typeof env["REPLIT_USER"] === "string",
},
// Devin (Cognition).
{
name: "devin",
check: (env) => typeof env["DEVIN_SESSION_ID"] === "string",
},
// Aider.
{
name: "aider",
check: (env) => typeof env["AIDER_RUN_ID"] === "string",
},
// Gemini CLI — sets a known env var when invoking shell tools.
{
name: "gemini_cli",
check: (env) => typeof env["GEMINI_CLI"] === "string",
},
// Nous Research Hermes Agent — cli.py:50 unconditionally executes
// os.environ["HERMES_QUIET"] = "1"
// at module load, so the marker propagates via os.environ to every
@@ -125,6 +116,14 @@ const VENDOR_RULES: VendorRule[] = [
typeof env["OPENCLAW_STATE_DIR"] === "string" ||
typeof env["OPENCLAW_CONFIG_PATH"] === "string",
},
// Pi coding agent (https://pi.dev, 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.
{
name: "pi",
check: (env) => typeof env["PI_CODING_AGENT"] === "string",
},
];
/**
+5 -3
View File
@@ -39,9 +39,11 @@ export interface SystemMeta {
sandbox_runtime: SandboxRuntime;
/**
* Coding-agent vendor that spawned this process, if any (claude_code,
* codex, cursor, copilot_agent, jules, replit, devin, aider, gemini_cli).
* Detected by env-var existence only — values are never read. null when
* no agent is detected (i.e. a human invoked the CLI directly).
* codex, cursor, copilot_agent, replit, hermes, openclaw, pi).
* Detected by env-var existence only — values are never read. Every rule
* keys on a marker that has a public-source citation in agent_runtime.ts;
* unverified guesses are deliberately omitted (false-negative > guess).
* null when no agent is detected.
*/
agent_runtime: AgentRuntime;
}