Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
npx hyperframes telemetry status
```

Telemetry collects command names, render performance, example choices, and system info. It does **not** collect file paths, project names, video content, or personally identifiable information. Disable with `HYPERFRAMES_NO_TELEMETRY=1` or the command above.
Telemetry collects command names, render performance, example choices, and system info — including a coarse environment fingerprint (OS, kernel string, CPU/memory shape, sandbox runtime such as gVisor or Docker, and the *name* of a coding agent driving the CLI when one is detected, e.g. `claude_code` / `codex` / `cursor`). The agent name is derived from the existence of well-known environment variables; their values are never read. Telemetry does **not** collect file paths, project names, video content, environment variable values, or personally identifiable information. Disable with `HYPERFRAMES_NO_TELEMETRY=1` or the command above.

### `skills`

Expand Down
202 changes: 202 additions & 0 deletions packages/cli/src/telemetry/agent_runtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";

// agent_runtime.ts reads node:os via release/platform and node:fs for the
// /proc files. detectAgentRuntime is exercised by mutating process.env;
// detectSandboxRuntime is exercised through a small set of node:os mocks.

const VENDOR_ENV_KEYS = [
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CODEX_HOME",
"CODEX_SANDBOX",
"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",
] as const;

function stripVendorEnv(): void {
for (const key of VENDOR_ENV_KEYS) delete process.env[key];
}

describe("detectAgentRuntime — base behavior", () => {
const savedEnv = { ...process.env };
beforeEach(stripVendorEnv);
afterEach(() => {
process.env = { ...savedEnv };
});

it("returns null on a plain shell with no agent markers", async () => {
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBeNull();
});

it("first matching vendor wins (rule order)", async () => {
// 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";
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_API_KEY"] = "sk-supersecret-DO-NOT-LEAK";
const { detectAgentRuntime } = await import("./agent_runtime.js");
const result = detectAgentRuntime();
expect(result).toBe("codex");
expect(typeof result).toBe("string");
expect((result ?? "").includes("supersecret")).toBe(false);
});
});

describe("detectAgentRuntime — Claude Code", () => {
const savedEnv = { ...process.env };
beforeEach(stripVendorEnv);
afterEach(() => {
process.env = { ...savedEnv };
});

it("detects via CLAUDECODE=1", async () => {
process.env["CLAUDECODE"] = "1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("claude_code");
});

it("detects via CLAUDE_CODE_ENTRYPOINT", async () => {
process.env["CLAUDE_CODE_ENTRYPOINT"] = "cli";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("claude_code");
});
});

describe("detectAgentRuntime — OpenAI Codex", () => {
const savedEnv = { ...process.env };
beforeEach(stripVendorEnv);
afterEach(() => {
process.env = { ...savedEnv };
});

it("detects via CODEX_HOME", async () => {
process.env["CODEX_HOME"] = "/home/codex";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("codex");
});

it("detects via CODEX_SANDBOX_NETWORK_DISABLED", async () => {
process.env["CODEX_SANDBOX_NETWORK_DISABLED"] = "1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("codex");
});
});

describe("detectAgentRuntime — Cursor / Copilot / cohort", () => {
const savedEnv = { ...process.env };
beforeEach(stripVendorEnv);
afterEach(() => {
process.env = { ...savedEnv };
});

it("detects Cursor via TERM_PROGRAM=cursor", async () => {
process.env["TERM_PROGRAM"] = "cursor";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("cursor");
});

it("detects Copilot Coding Agent via GITHUB_ACTIONS + COPILOT_AGENT_ID", async () => {
process.env["GITHUB_ACTIONS"] = "true";
process.env["COPILOT_AGENT_ID"] = "abc123";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("copilot_agent");
});

it("does NOT flag generic GitHub Actions as copilot_agent", async () => {
process.env["GITHUB_ACTIONS"] = "true";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBeNull();
});
});

describe("detectAgentRuntime — Jules / Replit / Devin / Hermes / openclaw", () => {
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 () => {
process.env["HERMES_QUIET"] = "1";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("hermes");
});

it("detects openclaw via inherited OPENCLAW_STATE_DIR", async () => {
process.env["OPENCLAW_STATE_DIR"] = "/tmp/openclaw";
const { detectAgentRuntime } = await import("./agent_runtime.js");
expect(detectAgentRuntime()).toBe("openclaw");
});
});

describe("detectSandboxRuntime — kernel-string path", () => {
beforeEach(() => {
vi.resetModules();
});

afterEach(() => {
vi.resetModules();
vi.restoreAllMocks();
});

it("reports gvisor for a 4.19.0-gvisor kernel string", async () => {
vi.doMock("node:os", async () => {
const actual = await vi.importActual<typeof import("node:os")>("node:os");
return { ...actual, release: () => "4.19.0-gvisor", platform: () => "linux" };
});
const { detectSandboxRuntime } = await import("./agent_runtime.js");
expect(detectSandboxRuntime()).toBe("gvisor");
});

it("reports gvisor for the legacy 4.4.0 Sentry kernel string", async () => {
vi.doMock("node:os", async () => {
const actual = await vi.importActual<typeof import("node:os")>("node:os");
return { ...actual, release: () => "4.4.0", platform: () => "linux" };
});
const { detectSandboxRuntime } = await import("./agent_runtime.js");
expect(detectSandboxRuntime()).toBe("gvisor");
});
});
Loading
Loading