context-mode 1.0.123 → 1.0.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.123"
9
+ "version": "1.0.125"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "1.0.123",
16
+ "version": "1.0.125",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.123",
3
+ "version": "1.0.125",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.123",
6
+ "version": "1.0.125",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.123",
3
+ "version": "1.0.125",
4
4
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
package/README.md CHANGED
@@ -570,7 +570,7 @@ Full documentation: [`docs/adapters/openclaw.md`](docs/adapters/openclaw.md)
570
570
  ```json
571
571
  {
572
572
  "hooks": {
573
- "PreToolUse": [{ "matcher": "local_shell|shell|shell_command|exec_command|container.exec|functions\\.exec_command|Bash|Shell|apply_patch|functions\\.apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__.*__ctx_execute|mcp__.*__ctx_execute_file|mcp__.*__ctx_batch_execute|mcp__.*__ctx_fetch_and_index|mcp__.*__ctx_search|mcp__.*__ctx_index|mcp__(?!.*context-mode)", "hooks": [{ "type": "command", "command": "context-mode hook codex pretooluse" }] }],
573
+ "PreToolUse": [{ "matcher": "local_shell|shell|shell_command|exec_command|Bash|Shell|apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__", "hooks": [{ "type": "command", "command": "context-mode hook codex pretooluse" }] }],
574
574
  "PostToolUse": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex posttooluse" }] }],
575
575
  "SessionStart": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex sessionstart" }] }],
576
576
  "PreCompact": [{ "hooks": [{ "type": "command", "command": "context-mode hook codex precompact" }] }],
@@ -964,7 +964,7 @@ npm install -g context-mode
964
964
  | Tool | What it does | Context saved |
965
965
  |---|---|---|
966
966
  | `ctx_batch_execute` | Run multiple commands + search multiple queries in ONE call. Opt-in `concurrency: 1-8` for I/O-bound batches. | 986 KB → 62 KB |
967
- | `ctx_execute` | Run code in 11 languages. Only stdout enters context. | 56 KB → 299 B |
967
+ | `ctx_execute` | Run code in 12 languages. Only stdout enters context. | 56 KB → 299 B |
968
968
  | `ctx_execute_file` | Process files in sandbox. Raw content never leaves. | 45 KB → 155 B |
969
969
  | `ctx_index` | Chunk markdown into FTS5 with BM25 ranking. | 60 KB → 40 B |
970
970
  | `ctx_search` | Query indexed content with multiple queries in one call. | On-demand retrieval |
@@ -978,7 +978,7 @@ npm install -g context-mode
978
978
 
979
979
  Each `ctx_execute` call spawns an isolated subprocess with its own process boundary. Scripts can't access each other's memory or state. The subprocess runs your code, captures stdout, and only that stdout enters the conversation context. The raw data — log files, API responses, snapshots — never leaves the sandbox.
980
980
 
981
- Eleven language runtimes are available: JavaScript, TypeScript, Python, Shell, Ruby, Go, Rust, PHP, Perl, R, and Elixir. Bun is auto-detected for 3-5x faster JS/TS execution.
981
+ Twelve language runtimes are available: JavaScript, TypeScript, Python, Shell, Ruby, Go, Rust, PHP, Perl, R, Elixir, and C#. Bun is auto-detected for 3-5x faster JS/TS execution.
982
982
 
983
983
  Authenticated CLIs work through credential passthrough — `gh`, `aws`, `gcloud`, `kubectl`, `docker` inherit environment variables and config paths without exposing them to the conversation.
984
984
 
@@ -25,22 +25,21 @@ export declare const HOOK_TYPES: {
25
25
  };
26
26
  export type HookType = (typeof HOOK_TYPES)[keyof typeof HOOK_TYPES];
27
27
  /**
28
- * Negative-lookahead matcher for external MCP tool namespaces (#529).
28
+ * External MCP catch-all matcher for Claude Code (#529, #547 hotfix).
29
29
  *
30
- * Claude Code's hook matcher engine evaluates each entry as a regex against
31
- * the tool name. This pattern fires on any `mcp__<server>__<tool>` whose
32
- * server segment is NOT context-mode's own (`plugin_context-mode_...`).
33
- * Without it, large payloads from external MCPs (slack channel history,
34
- * telegram messages, gdrive content, notion pages, …) bypass PreToolUse
35
- * routing and flood the model's context window PostToolUse runs too late
36
- * to keep the raw data out.
37
- *
38
- * The negative lookahead prevents this entry from double-firing on
39
- * context-mode's own ctx_* tools, which already have dedicated entries above.
30
+ * Claude Code's hook matcher engine treats this entry as a substring match
31
+ * (it also accepts regex, but `mcp__` alone is enough — every MCP tool
32
+ * surfaces as `mcp__<server>__<tool>`). v1.0.124 used a negative lookahead
33
+ * `mcp__(?!plugin_context-mode_)` to skip context-mode's own MCP tools,
34
+ * but this same hooks.json is bundled to Codex CLI which uses Rust's
35
+ * `regex` crate (no look-around support) Codex rejected the matcher at
36
+ * boot, breaking every Codex user (#547). Drop the lookaround on both
37
+ * sides; the hook BODY (`isExternalMcpTool()` in hooks/core/routing.mjs)
38
+ * already filters context-mode's own tools, so semantics are preserved.
40
39
  */
41
- export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__(?!plugin_context-mode_)";
40
+ export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
42
41
  /** Tools that context-mode's PreToolUse hook intercepts. */
43
- export declare const PRE_TOOL_USE_MATCHERS: readonly ["Bash", "WebFetch", "Read", "Grep", "Agent", "mcp__plugin_context-mode_context-mode__ctx_execute", "mcp__plugin_context-mode_context-mode__ctx_execute_file", "mcp__plugin_context-mode_context-mode__ctx_batch_execute", "mcp__(?!plugin_context-mode_)"];
42
+ export declare const PRE_TOOL_USE_MATCHERS: readonly ["Bash", "WebFetch", "Read", "Grep", "Agent", "mcp__plugin_context-mode_context-mode__ctx_execute", "mcp__plugin_context-mode_context-mode__ctx_execute_file", "mcp__plugin_context-mode_context-mode__ctx_batch_execute", "mcp__"];
44
43
  /**
45
44
  * Combined matcher pattern for settings.json (pipe-separated).
46
45
  * Used by the upgrade command when writing a single consolidated entry.
@@ -31,20 +31,19 @@ export const HOOK_TYPES = {
31
31
  // PreToolUse matchers
32
32
  // ─────────────────────────────────────────────────────────
33
33
  /**
34
- * Negative-lookahead matcher for external MCP tool namespaces (#529).
34
+ * External MCP catch-all matcher for Claude Code (#529, #547 hotfix).
35
35
  *
36
- * Claude Code's hook matcher engine evaluates each entry as a regex against
37
- * the tool name. This pattern fires on any `mcp__<server>__<tool>` whose
38
- * server segment is NOT context-mode's own (`plugin_context-mode_...`).
39
- * Without it, large payloads from external MCPs (slack channel history,
40
- * telegram messages, gdrive content, notion pages, …) bypass PreToolUse
41
- * routing and flood the model's context window PostToolUse runs too late
42
- * to keep the raw data out.
43
- *
44
- * The negative lookahead prevents this entry from double-firing on
45
- * context-mode's own ctx_* tools, which already have dedicated entries above.
36
+ * Claude Code's hook matcher engine treats this entry as a substring match
37
+ * (it also accepts regex, but `mcp__` alone is enough — every MCP tool
38
+ * surfaces as `mcp__<server>__<tool>`). v1.0.124 used a negative lookahead
39
+ * `mcp__(?!plugin_context-mode_)` to skip context-mode's own MCP tools,
40
+ * but this same hooks.json is bundled to Codex CLI which uses Rust's
41
+ * `regex` crate (no look-around support) Codex rejected the matcher at
42
+ * boot, breaking every Codex user (#547). Drop the lookaround on both
43
+ * sides; the hook BODY (`isExternalMcpTool()` in hooks/core/routing.mjs)
44
+ * already filters context-mode's own tools, so semantics are preserved.
46
45
  */
47
- export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__(?!plugin_context-mode_)";
46
+ export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
48
47
  /** Tools that context-mode's PreToolUse hook intercepts. */
49
48
  export const PRE_TOOL_USE_MATCHERS = [
50
49
  "Bash",
@@ -67,7 +67,13 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
67
67
  return join(this.getConfigDir(), "settings.json");
68
68
  }
69
69
  generateHookConfig(pluginRoot) {
70
- const preToolUseCommand = `node ${pluginRoot}/hooks/pretooluse.mjs`;
70
+ // Paths must be double-quoted so that hosts (or our own diagnostic
71
+ // regex) parse them correctly when pluginRoot contains spaces — common
72
+ // on Windows where the user folder is e.g. "C:\Users\First Last\...".
73
+ // Without quotes, extractHookScriptPath's `\S+\.mjs` fallback grabs
74
+ // only the tail after the last space, producing a doubled-path FAIL
75
+ // in `ctx doctor`. Matches the quoting style used in hooks/hooks.json.
76
+ const preToolUseCommand = `node "${pluginRoot}/hooks/pretooluse.mjs"`;
71
77
  const preToolUseMatchers = [...PRE_TOOL_USE_MATCHERS];
72
78
  return {
73
79
  PreToolUse: preToolUseMatchers.map((matcher) => ({
@@ -80,7 +86,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
80
86
  hooks: [
81
87
  {
82
88
  type: "command",
83
- command: `node ${pluginRoot}/hooks/posttooluse.mjs`,
89
+ command: `node "${pluginRoot}/hooks/posttooluse.mjs"`,
84
90
  },
85
91
  ],
86
92
  },
@@ -91,7 +97,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
91
97
  hooks: [
92
98
  {
93
99
  type: "command",
94
- command: `node ${pluginRoot}/hooks/precompact.mjs`,
100
+ command: `node "${pluginRoot}/hooks/precompact.mjs"`,
95
101
  },
96
102
  ],
97
103
  },
@@ -102,7 +108,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
102
108
  hooks: [
103
109
  {
104
110
  type: "command",
105
- command: `node ${pluginRoot}/hooks/userpromptsubmit.mjs`,
111
+ command: `node "${pluginRoot}/hooks/userpromptsubmit.mjs"`,
106
112
  },
107
113
  ],
108
114
  },
@@ -113,7 +119,7 @@ export class ClaudeCodeAdapter extends ClaudeCodeBaseAdapter {
113
119
  hooks: [
114
120
  {
115
121
  type: "command",
116
- command: `node ${pluginRoot}/hooks/sessionstart.mjs`,
122
+ command: `node "${pluginRoot}/hooks/sessionstart.mjs"`,
117
123
  },
118
124
  ],
119
125
  },
@@ -27,24 +27,23 @@ export declare const HOOK_TYPES: {
27
27
  readonly STOP: "Stop";
28
28
  };
29
29
  /**
30
- * Negative-lookahead matcher for external MCP tool namespaces on Codex CLI (#529).
30
+ * External MCP catch-all matcher for Codex CLI (#529, #547 hotfix).
31
31
  *
32
32
  * Codex CLI's hook `tool_name` payload uses `mcp__<server>__<tool>` for any
33
- * MCP-namespaced tool verified by configs/codex/hooks.json which already
34
- * matches `mcp__.*__ctx_execute` style for context-mode's OWN MCP tools. This
35
- * pattern fires PreToolUse for any external `mcp__<server>__<tool>` whose
36
- * server segment does NOT contain `context-mode`. Without it, large payloads
37
- * from slack / telegram / gdrive / notion-style MCPs bypass the routing nudge
38
- * and flood the model's context — PostToolUse runs too late to keep raw data
39
- * out.
33
+ * MCP-namespaced tool. Originally this constant used a negative lookahead
34
+ * `mcp__(?!.*context-mode)` to exclude context-mode's own MCP tools at the
35
+ * matcher layer. v1.0.124 shipped that pattern and Codex (Rust `regex` crate)
36
+ * rejected the matcher at boot with "look-around not supported", breaking
37
+ * every Codex user (#547).
40
38
  *
41
- * The negative lookahead `(?!.*context-mode)` covers both naming variants
42
- * Codex sees in practice: the canonical `mcp__context-mode__ctx_*` AND the
43
- * Claude Code plugin shim `mcp__plugin_context-mode_context-mode__ctx_*`.
44
- * Codex own bare names (ctx_execute, local_shell, …) are not `mcp__`-prefixed
45
- * and are unaffected.
39
+ * Fix: drop the lookaround. The matcher is now a charset-clean literal
40
+ * (`[A-Za-z0-9_|]` only), satisfying Codex's `is_exact_matcher`
41
+ * (refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152) which
42
+ * short-circuits the regex engine entirely. context-mode's own MCP tools are
43
+ * already filtered in the hook BODY by `isExternalMcpTool()` in
44
+ * hooks/core/routing.mjs — semantics preserved.
46
45
  */
47
- export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__(?!.*context-mode)";
46
+ export declare const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
48
47
  /**
49
48
  * Path to the routing instructions file for Codex CLI.
50
49
  * Used as fallback routing awareness alongside hook-based enforcement.
@@ -33,24 +33,23 @@ export const HOOK_TYPES = {
33
33
  // External MCP routing matcher (#529)
34
34
  // ─────────────────────────────────────────────────────────
35
35
  /**
36
- * Negative-lookahead matcher for external MCP tool namespaces on Codex CLI (#529).
36
+ * External MCP catch-all matcher for Codex CLI (#529, #547 hotfix).
37
37
  *
38
38
  * Codex CLI's hook `tool_name` payload uses `mcp__<server>__<tool>` for any
39
- * MCP-namespaced tool verified by configs/codex/hooks.json which already
40
- * matches `mcp__.*__ctx_execute` style for context-mode's OWN MCP tools. This
41
- * pattern fires PreToolUse for any external `mcp__<server>__<tool>` whose
42
- * server segment does NOT contain `context-mode`. Without it, large payloads
43
- * from slack / telegram / gdrive / notion-style MCPs bypass the routing nudge
44
- * and flood the model's context — PostToolUse runs too late to keep raw data
45
- * out.
39
+ * MCP-namespaced tool. Originally this constant used a negative lookahead
40
+ * `mcp__(?!.*context-mode)` to exclude context-mode's own MCP tools at the
41
+ * matcher layer. v1.0.124 shipped that pattern and Codex (Rust `regex` crate)
42
+ * rejected the matcher at boot with "look-around not supported", breaking
43
+ * every Codex user (#547).
46
44
  *
47
- * The negative lookahead `(?!.*context-mode)` covers both naming variants
48
- * Codex sees in practice: the canonical `mcp__context-mode__ctx_*` AND the
49
- * Claude Code plugin shim `mcp__plugin_context-mode_context-mode__ctx_*`.
50
- * Codex own bare names (ctx_execute, local_shell, …) are not `mcp__`-prefixed
51
- * and are unaffected.
45
+ * Fix: drop the lookaround. The matcher is now a charset-clean literal
46
+ * (`[A-Za-z0-9_|]` only), satisfying Codex's `is_exact_matcher`
47
+ * (refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152) which
48
+ * short-circuits the regex engine entirely. context-mode's own MCP tools are
49
+ * already filtered in the hook BODY by `isExternalMcpTool()` in
50
+ * hooks/core/routing.mjs — semantics preserved.
52
51
  */
53
- export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__(?!.*context-mode)";
52
+ export const EXTERNAL_MCP_MATCHER_PATTERN = "mcp__";
54
53
  // ─────────────────────────────────────────────────────────
55
54
  // Routing instructions
56
55
  // ─────────────────────────────────────────────────────────
@@ -18,14 +18,25 @@ import { resolve, dirname, join } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { BaseAdapter } from "../base.js";
20
20
  import { resolveCodexConfigDir } from "./paths.js";
21
- // PreToolUse matcher: canonical Codex tool names + context-mode own MCP tools
22
- // (both bare and `mcp__<server>__<tool>` forms) + external MCP catch-all (#529).
23
- // The final `mcp__(?!.*context-mode)` segment uses a negative lookahead that
24
- // excludes any `mcp__` tool whose server segment contains `context-mode` so
25
- // context-mode's own MCP tools (already wired by the explicit entries above)
26
- // are not double-routed. Keep this as a single string literal `codex.test.ts`
27
- // drift-guard parses the source with a `"([^"]+)"` regex.
28
- const PRE_TOOL_USE_MATCHER_PATTERN = "local_shell|shell|shell_command|exec_command|container.exec|functions\\.exec_command|Bash|Shell|apply_patch|functions\\.apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__.*__ctx_execute|mcp__.*__ctx_execute_file|mcp__.*__ctx_batch_execute|mcp__.*__ctx_fetch_and_index|mcp__.*__ctx_search|mcp__.*__ctx_index|mcp__(?!.*context-mode)";
21
+ // PreToolUse matcher: canonical Codex tool names + context-mode bare MCP tool
22
+ // names + external MCP catch-all literal (#529, #547 hotfix).
23
+ //
24
+ // Codex CLI's Rust `regex` crate does NOT support look-around, and
25
+ // `is_exact_matcher` (refs/platforms/codex/codex-rs/hooks/src/events/common.rs:152)
26
+ // short-circuits the regex engine entirely when the matcher contains only
27
+ // [A-Za-z0-9_|]. v1.0.124 shipped a matcher with `(?!.*context-mode)` AND
28
+ // `mcp__.*__ctx_*` regex syntax — Codex rejected the file at boot with
29
+ // "look-around not supported" → all v1.0.124 Codex users broken (#547).
30
+ //
31
+ // Fix: keep only literal tool names (charset-clean). The hook BODY already
32
+ // filters context-mode's own MCP tools via `isExternalMcpTool()` in
33
+ // hooks/core/routing.mjs, so dropping `mcp__.*__ctx_*` and the lookaround
34
+ // preserves end-to-end semantics. The literal `mcp__` final segment is a
35
+ // no-op under exact-matcher mode but kept for parity with hooks/hooks.json.
36
+ //
37
+ // Keep this as a single string literal — `codex.test.ts` drift-guard parses
38
+ // the source with a `"([^"]+)"` regex.
39
+ const PRE_TOOL_USE_MATCHER_PATTERN = "local_shell|shell|shell_command|exec_command|Bash|Shell|apply_patch|Edit|Write|grep_files|ctx_execute|ctx_execute_file|ctx_batch_execute|ctx_fetch_and_index|ctx_search|ctx_index|mcp__";
29
40
  const CODEX_HOOK_COMMANDS = {
30
41
  PreToolUse: "context-mode hook codex pretooluse",
31
42
  PostToolUse: "context-mode hook codex posttooluse",
@@ -29,11 +29,55 @@ export declare function __resetClaudeCodePluginCacheForTests(): void;
29
29
  */
30
30
  export declare function __seedClaudeCodePluginCacheMissForTests(): void;
31
31
  /**
32
- * High-confidence env vars per platform, checked in priority order.
33
- * Single source of truth consumed by detectPlatform() below and by
34
- * tests that need to clear platform-related env vars deterministically.
32
+ * Tag for each PLATFORM_ENV_VARS row.
33
+ * - `workspace`: env var names a project/working directory. Used by
34
+ * `resolveProjectDir({ strictPlatform })` to form the candidate list,
35
+ * and by Pi's bridge to scrub foreign workspace vars on child spawn.
36
+ * - `identification`: env var only signals which host is running; carries
37
+ * no project path. NEVER scrubbed (some are load-bearing, e.g.
38
+ * CLAUDE_PLUGIN_ROOT for hook integrations).
39
+ *
40
+ * Issue #545 — algorithmic env-leak fix. The split allows resolveProjectDir
41
+ * to derive ALLOW (own workspace vars) and BAN (other platforms' workspace
42
+ * vars) sets from a single registry, satisfying MUST-3 (15 adapters equal).
43
+ */
44
+ export type EnvVarRole = "workspace" | "identification";
45
+ export interface PlatformEnvEntry {
46
+ readonly name: string;
47
+ readonly role: EnvVarRole;
48
+ /**
49
+ * When `false`, this entry is NOT used as a high-confidence detection
50
+ * signal — only consumed by `workspaceEnvVarsFor`/`foreignWorkspaceEnv`
51
+ * (project-dir cascade and bridge env scrub). Use for consumer-set
52
+ * workspace vars that the host runtime never emits itself, so that a
53
+ * stale env var on an unrelated host does not misclassify the platform.
54
+ * Default: `true` (entry participates in detection).
55
+ *
56
+ * Issue #542 — PI_PROJECT_DIR / PI_WORKSPACE_DIR are consumer-set and
57
+ * MUST NOT trigger Pi detection on their own.
58
+ */
59
+ readonly detect?: boolean;
60
+ }
61
+ export declare const PLATFORM_ENV_VARS: ReadonlyMap<PlatformId, readonly PlatformEnvEntry[]>;
62
+ /**
63
+ * Backwards-compat shim: legacy `string[]` shape used by detection logic and
64
+ * by tests that iterate the registry to clear env vars. Always returns the
65
+ * names in registry order.
66
+ */
67
+ export declare function getEnvVarNames(platform: PlatformId): string[];
68
+ /**
69
+ * Issue #545 — return only role=workspace env var names for a platform, in
70
+ * registry order. Empty array for adapters with no workspace var (e.g.
71
+ * codex, kilo, zed, antigravity, openclaw, kiro). Consumed by
72
+ * `resolveProjectDir({ strictPlatform })` to build the cascade.
73
+ */
74
+ export declare function workspaceEnvVarsFor(platform: PlatformId): string[];
75
+ /**
76
+ * Issue #545 — return the union of workspace env vars from ALL platforms
77
+ * EXCEPT the given one. Consumed by Pi's bridge env scrub (strip foreign
78
+ * workspace vars from spawned MCP child) and by the matrix regression test.
35
79
  */
36
- export declare const PLATFORM_ENV_VARS: readonly [readonly ["claude-code", readonly ["CLAUDE_CODE_ENTRYPOINT", "CLAUDE_PLUGIN_ROOT", "CLAUDE_PROJECT_DIR", "CLAUDE_SESSION_ID"]], readonly ["antigravity", readonly ["ANTIGRAVITY_CLI_ALIAS"]], readonly ["cursor", readonly ["CURSOR_TRACE_ID", "CURSOR_CLI"]], readonly ["kilo", readonly ["KILO", "KILO_PID"]], readonly ["opencode", readonly ["OPENCODE", "OPENCODE_PID"]], readonly ["zed", readonly ["ZED_SESSION_ID", "ZED_TERM"]], readonly ["codex", readonly ["CODEX_THREAD_ID", "CODEX_CI"]], readonly ["gemini-cli", readonly ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]], readonly ["vscode-copilot", readonly ["VSCODE_PID", "VSCODE_CWD"]], readonly ["jetbrains-copilot", readonly ["IDEA_INITIAL_DIRECTORY"]], readonly ["qwen-code", readonly ["QWEN_PROJECT_DIR"]], readonly ["omp", readonly ["PI_CODING_AGENT_DIR"]], readonly ["pi", readonly ["PI_CONFIG_DIR", "PI_SESSION_FILE", "PI_COMPILED"]]];
80
+ export declare function foreignWorkspaceEnv(platform: PlatformId): Set<string>;
37
81
  /**
38
82
  * Sync map from platform identifier → home-relative path segments where that
39
83
  * platform stores its config. Mirrors the `super([...])` argument passed by
@@ -59,10 +59,15 @@ export function __seedClaudeCodePluginCacheMissForTests() {
59
59
  }
60
60
  /**
61
61
  * High-confidence env vars per platform, checked in priority order.
62
- * Single source of truth — consumed by detectPlatform() below and by
63
- * tests that need to clear platform-related env vars deterministically.
62
+ * Single source of truth — consumed by detectPlatform() below, by
63
+ * `resolveProjectDir({ strictPlatform })` for cascade construction, and by
64
+ * Pi's bridge env scrub. Tests also iterate this map to clear platform-
65
+ * related env vars deterministically.
66
+ *
67
+ * The map shape is `Map<PlatformId, ReadonlyArray<PlatformEnvEntry>>`. Use
68
+ * `getEnvVarNames(p)` to get just the names (legacy `string[]` shape).
64
69
  */
65
- export const PLATFORM_ENV_VARS = [
70
+ const _PLATFORM_ENV_VARS_RAW = [
66
71
  // Order matters: forks listed BEFORE the fork's parent so collision
67
72
  // detection works. Every entry verified against platform's own runtime
68
73
  // source code (PR #376 follow-up: full audit, May 2026 — see git blame).
@@ -75,49 +80,84 @@ export const PLATFORM_ENV_VARS = [
75
80
  // are the disambiguators for issue #539 (Claude Code running inside a
76
81
  // VS Code integrated terminal that has VSCODE_PID set). They MUST be
77
82
  // checked here so detect resolves to claude-code BEFORE falling through
78
- // to vscode-copilot at line 70 below.
83
+ // to vscode-copilot below.
79
84
  ["claude-code", [
80
- "CLAUDE_CODE_ENTRYPOINT",
81
- "CLAUDE_PLUGIN_ROOT",
82
- "CLAUDE_PROJECT_DIR",
83
- "CLAUDE_SESSION_ID",
85
+ { name: "CLAUDE_CODE_ENTRYPOINT", role: "identification" },
86
+ { name: "CLAUDE_PLUGIN_ROOT", role: "identification" },
87
+ { name: "CLAUDE_PROJECT_DIR", role: "workspace" },
88
+ { name: "CLAUDE_SESSION_ID", role: "identification" },
84
89
  ]],
85
90
  // antigravity (Electron/VSCode fork) — google-gemini/gemini-cli
86
91
  // packages/core/src/ide/detect-ide.ts checks ANTIGRAVITY_CLI_ALIAS as the
87
92
  // canonical Antigravity marker. Listed before vscode-copilot.
88
- ["antigravity", ["ANTIGRAVITY_CLI_ALIAS"]],
93
+ ["antigravity", [
94
+ { name: "ANTIGRAVITY_CLI_ALIAS", role: "identification" },
95
+ ]],
89
96
  // cursor (VSCode fork) — listed before vscode-copilot. CURSOR_TRACE_ID has
90
97
  // 800+ hits in major OSS detection libs (Vercel Next.js, Bun, Google
91
- // gemini-cli, Nx, CrewAI).
92
- ["cursor", ["CURSOR_TRACE_ID", "CURSOR_CLI"]],
98
+ // gemini-cli, Nx, CrewAI). CURSOR_CWD is the documented workspace var
99
+ // (issue #521) — listed first so workspace cascade picks it up.
100
+ ["cursor", [
101
+ { name: "CURSOR_CWD", role: "workspace" },
102
+ { name: "CURSOR_TRACE_ID", role: "identification" },
103
+ { name: "CURSOR_CLI", role: "identification" },
104
+ ]],
93
105
  // kilo (OpenCode fork) — Kilo-Org/kilocode packages/opencode/src/index.ts:138 + 139
94
- // sets `process.env.KILO = 1` + `process.env.KILO_PID = String(process.pid)`.
95
- ["kilo", ["KILO", "KILO_PID"]],
106
+ // sets `process.env.KILO = 1` + `process.env.KILO_PID = String(process.pid)`.
107
+ ["kilo", [
108
+ { name: "KILO", role: "identification" },
109
+ { name: "KILO_PID", role: "identification" },
110
+ ]],
96
111
  // opencode — sst/opencode packages/opencode/src/index.ts:108-109 sets
97
112
  // OPENCODE=1 + OPENCODE_PID=<pid> on every CLI invocation.
98
- ["opencode", ["OPENCODE", "OPENCODE_PID"]],
113
+ // OPENCODE_PROJECT_DIR is the documented workspace var (consumed by the
114
+ // legacy resolver cascade) — listed first so the workspace cascade picks
115
+ // it up under strict mode.
116
+ ["opencode", [
117
+ { name: "OPENCODE_PROJECT_DIR", role: "workspace" },
118
+ { name: "OPENCODE", role: "identification" },
119
+ { name: "OPENCODE_PID", role: "identification" },
120
+ ]],
99
121
  // zed — zed-industries/zed crates/terminal/src/terminal.rs sets ZED_TERM=true
100
122
  // in `insert_zed_terminal_env()`. Google's gemini-cli uses ZED_SESSION_ID.
101
- ["zed", ["ZED_SESSION_ID", "ZED_TERM"]],
123
+ ["zed", [
124
+ { name: "ZED_SESSION_ID", role: "identification" },
125
+ { name: "ZED_TERM", role: "identification" },
126
+ ]],
102
127
  // codex — openai/codex codex-rs/core/src/exec_env.rs sets CODEX_THREAD_ID
103
128
  // per exec; unified_exec/process_manager.rs sets CODEX_CI in CI mode.
104
- ["codex", ["CODEX_THREAD_ID", "CODEX_CI"]],
129
+ ["codex", [
130
+ { name: "CODEX_THREAD_ID", role: "identification" },
131
+ { name: "CODEX_CI", role: "identification" },
132
+ ]],
105
133
  // gemini-cli — GEMINI_PROJECT_DIR per google-gemini/gemini-cli
106
134
  // docs/hooks/index.md; GEMINI_CLI is the MCP-server sentinel.
107
- ["gemini-cli", ["GEMINI_PROJECT_DIR", "GEMINI_CLI"]],
135
+ ["gemini-cli", [
136
+ { name: "GEMINI_PROJECT_DIR", role: "workspace" },
137
+ { name: "GEMINI_CLI", role: "identification" },
138
+ ]],
108
139
  // vscode-copilot — VSCODE_PID + VSCODE_CWD set by microsoft/vscode bootstrap.
109
140
  // Listed AFTER cursor and antigravity since they inherit these vars as forks.
110
- ["vscode-copilot", ["VSCODE_PID", "VSCODE_CWD"]],
141
+ ["vscode-copilot", [
142
+ { name: "VSCODE_CWD", role: "workspace" },
143
+ { name: "VSCODE_PID", role: "identification" },
144
+ ]],
111
145
  // jetbrains-copilot — IDEA_INITIAL_DIRECTORY set by JetBrains launcher.
112
146
  // (IDEA_HOME and JETBRAINS_CLIENT_ID removed — no source-line evidence.)
113
- ["jetbrains-copilot", ["IDEA_INITIAL_DIRECTORY"]],
147
+ ["jetbrains-copilot", [
148
+ { name: "IDEA_INITIAL_DIRECTORY", role: "workspace" },
149
+ ]],
114
150
  // qwen-code — QWEN_PROJECT_DIR per QwenLM/qwen-code docs/users/features/hooks.md.
115
151
  // (QWEN_SESSION_ID removed — 0 hits in qwen-code repository.)
116
- ["qwen-code", ["QWEN_PROJECT_DIR"]],
152
+ ["qwen-code", [
153
+ { name: "QWEN_PROJECT_DIR", role: "workspace" },
154
+ ]],
117
155
  // omp (can1357/oh-my-pi). PI_CODING_AGENT_DIR is the upstream
118
156
  // agent-dir override per `packages/utils/src/dirs.ts:193`. Listed
119
157
  // BEFORE pi so OMP is not misclassified as Pi when both are installed.
120
- ["omp", ["PI_CODING_AGENT_DIR"]],
158
+ ["omp", [
159
+ { name: "PI_CODING_AGENT_DIR", role: "workspace" },
160
+ ]],
121
161
  // pi — Issue #542 marker correction. PI_PROJECT_DIR is a consumer-set
122
162
  // var (read by src/adapters/pi/extension.ts) but is NOT auto-set by
123
163
  // the Pi runtime — verified against
@@ -126,11 +166,62 @@ export const PLATFORM_ENV_VARS = [
126
166
  // PI_CONFIG_DIR (config dir override), PI_SESSION_FILE (active session
127
167
  // path), and PI_COMPILED (binary build marker). PI_CODING_AGENT_DIR is
128
168
  // owned by OMP above; keep it there.
129
- ["pi", ["PI_CONFIG_DIR", "PI_SESSION_FILE", "PI_COMPILED"]],
169
+ //
170
+ // Issue #545 — PI_WORKSPACE_DIR / PI_PROJECT_DIR are workspace vars set
171
+ // by Pi's bridge so the resolver picks them up under strict mode.
172
+ // PI_WORKSPACE_DIR comes first (extension-set, freshest) before
173
+ // PI_PROJECT_DIR (user override) per registry-author cascade order.
174
+ ["pi", [
175
+ // Issue #545 — workspace vars set by Pi's bridge so resolveProjectDir
176
+ // under strict mode picks them up. detect=false because PI_*_DIR are
177
+ // consumer-set and must NOT misclassify a non-Pi host as Pi (#542).
178
+ { name: "PI_WORKSPACE_DIR", role: "workspace", detect: false },
179
+ { name: "PI_PROJECT_DIR", role: "workspace", detect: false },
180
+ { name: "PI_CONFIG_DIR", role: "identification" },
181
+ { name: "PI_SESSION_FILE", role: "identification" },
182
+ { name: "PI_COMPILED", role: "identification" },
183
+ ]],
130
184
  // openclaw — removed (runtime never sets OPENCLAW_HOME or OPENCLAW_CLI;
131
185
  // detection falls through to ~/.openclaw/ config-dir tier below).
132
186
  // kiro — not listed (no auto-set process env vars; ~/.kiro/ config-dir tier).
133
187
  ];
188
+ export const PLATFORM_ENV_VARS = new Map(_PLATFORM_ENV_VARS_RAW);
189
+ /**
190
+ * Backwards-compat shim: legacy `string[]` shape used by detection logic and
191
+ * by tests that iterate the registry to clear env vars. Always returns the
192
+ * names in registry order.
193
+ */
194
+ export function getEnvVarNames(platform) {
195
+ return (PLATFORM_ENV_VARS.get(platform) ?? []).map((e) => e.name);
196
+ }
197
+ /**
198
+ * Issue #545 — return only role=workspace env var names for a platform, in
199
+ * registry order. Empty array for adapters with no workspace var (e.g.
200
+ * codex, kilo, zed, antigravity, openclaw, kiro). Consumed by
201
+ * `resolveProjectDir({ strictPlatform })` to build the cascade.
202
+ */
203
+ export function workspaceEnvVarsFor(platform) {
204
+ return (PLATFORM_ENV_VARS.get(platform) ?? [])
205
+ .filter((e) => e.role === "workspace")
206
+ .map((e) => e.name);
207
+ }
208
+ /**
209
+ * Issue #545 — return the union of workspace env vars from ALL platforms
210
+ * EXCEPT the given one. Consumed by Pi's bridge env scrub (strip foreign
211
+ * workspace vars from spawned MCP child) and by the matrix regression test.
212
+ */
213
+ export function foreignWorkspaceEnv(platform) {
214
+ const ban = new Set();
215
+ for (const [p, vars] of PLATFORM_ENV_VARS) {
216
+ if (p === platform)
217
+ continue;
218
+ for (const v of vars) {
219
+ if (v.role === "workspace")
220
+ ban.add(v.name);
221
+ }
222
+ }
223
+ return ban;
224
+ }
134
225
  /**
135
226
  * Sync map from platform identifier → home-relative path segments where that
136
227
  * platform stores its config. Mirrors the `super([...])` argument passed by
@@ -204,7 +295,7 @@ export function detectPlatform(clientInfo) {
204
295
  }
205
296
  // ── High confidence: environment variables ─────────────
206
297
  for (const [platform, vars] of PLATFORM_ENV_VARS) {
207
- if (vars.some((v) => process.env[v])) {
298
+ if (vars.some((v) => v.detect !== false && process.env[v.name])) {
208
299
  // Issue #539 belt-and-suspenders: VSCODE_PID/VSCODE_CWD are exported
209
300
  // by VS Code into EVERY child process — including a Claude Code CLI
210
301
  // launched from the integrated terminal. If env vars alone want to
@@ -224,7 +315,7 @@ export function detectPlatform(clientInfo) {
224
315
  return {
225
316
  platform,
226
317
  confidence: "high",
227
- reason: `${vars.join(" or ")} env var set`,
318
+ reason: `${vars.filter((v) => v.detect !== false).map((v) => v.name).join(" or ")} env var set`,
228
319
  };
229
320
  }
230
321
  }
@@ -102,7 +102,7 @@ function getPlatform() {
102
102
  for (const [platform, vars] of PLATFORM_ENV_VARS) {
103
103
  if (platform !== "kilo" && platform !== "opencode")
104
104
  continue;
105
- if (vars.some((v) => process.env[v])) {
105
+ if (vars.some((v) => process.env[v.name])) {
106
106
  return platform;
107
107
  }
108
108
  }