github-router 0.3.71 → 0.3.73

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.
@@ -1,3 +1,3 @@
1
- import { a as removeOwnClaudeConfigMirror, c as sweepStaleRuntimeFiles, i as isUnderClaudeConfigMirror, l as writeRuntimeFileSecure, n as ensureClaudeConfigMirror, o as sweepStaleClaudeConfigMirrors, r as ensurePaths, s as sweepStalePeerAgentMdFiles, t as PATHS } from "./paths-CoFnpNZl.js";
1
+ import { a as removeOwnClaudeConfigMirror, c as sweepStaleRuntimeFiles, i as isUnderClaudeConfigMirror, l as writeRuntimeFileSecure, n as ensureClaudeConfigMirror, o as sweepStaleClaudeConfigMirrors, r as ensurePaths, s as sweepStalePeerAgentMdFiles, t as PATHS } from "./paths-yJ97KlKp.js";
2
2
 
3
3
  export { PATHS, ensureClaudeConfigMirror, ensurePaths, isUnderClaudeConfigMirror, removeOwnClaudeConfigMirror, sweepStaleClaudeConfigMirrors, sweepStalePeerAgentMdFiles, sweepStaleRuntimeFiles, writeRuntimeFileSecure };
@@ -29,6 +29,24 @@ const PATHS = {
29
29
  },
30
30
  get TOOLBELT_BIN_DIR() {
31
31
  return path.join(appDir(), "bin");
32
+ },
33
+ get COLBERT_DIR() {
34
+ return path.join(appDir(), "colbert");
35
+ },
36
+ get COLBERT_BIN_DIR() {
37
+ return path.join(appDir(), "colbert", "bin");
38
+ },
39
+ get COLBERT_MODELS_DIR() {
40
+ return path.join(appDir(), "colbert", "models");
41
+ },
42
+ get COLBERT_ORT_DIR() {
43
+ return path.join(appDir(), "colbert", "onnxruntime");
44
+ },
45
+ get COLBERT_INDICES_DIR() {
46
+ return path.join(appDir(), "colbert", "indices");
47
+ },
48
+ get COLBERT_META_DIR() {
49
+ return path.join(appDir(), "colbert", "indices", ".gh-router-meta");
32
50
  }
33
51
  };
34
52
  /**
@@ -88,10 +106,15 @@ async function ensurePaths() {
88
106
  consola.debug("Peer-agent .md sweep skipped:", err);
89
107
  });
90
108
  await (async () => {
91
- await (await import("./lifecycle-C2kZwv-z.js")).sweepStaleWorktreesAtBoot();
109
+ await (await import("./lifecycle-CQlm3YlF.js")).sweepStaleWorktreesAtBoot();
92
110
  })().catch((err) => {
93
111
  consola.debug("Worker worktree boot sweep skipped:", err);
94
112
  });
113
+ await (async () => {
114
+ await (await import("./lifecycle-BL4rWSrT.js")).sweepStaleColbertMetaAtBoot();
115
+ })().catch((err) => {
116
+ consola.debug("ColBERT meta boot sweep skipped:", err);
117
+ });
95
118
  }
96
119
  const CLAUDE_HOME_POLICY = new Map([
97
120
  [".credentials.json", "ISOLATED"],
@@ -843,7 +866,7 @@ async function sweepStalePeerAgentMdFiles() {
843
866
  * to `PERSONAS_READ` / `PERSONAS_WRITE` in `peer-mcp-personas.ts` or a
844
867
  * new coordinator-style agent is added in `codex-mcp-config.ts`.
845
868
  */
846
- const PEER_AGENT_MD_FILENAME = /^peer-(\d+)-[0-9a-f]{8}-(?:codex-critic|codex-reviewer|gemini-critic|codex-implementer|peer-review-coordinator)\.md$/;
869
+ const PEER_AGENT_MD_FILENAME = /^peer-(\d+)-[0-9a-f]{8}-(?:codex-critic|codex-reviewer|gemini-critic|gemini-reviewer|opus-critic|codex-implementer|peer-review-coordinator)\.md$/;
847
870
  /**
848
871
  * Strict regex matching only per-launch claude-config mirror dirs this
849
872
  * proxy creates: `<pid>-<8 hex>`. Anchored to the entire entry name so
@@ -914,4 +937,4 @@ async function removeOwnClaudeConfigMirror() {
914
937
 
915
938
  //#endregion
916
939
  export { removeOwnClaudeConfigMirror as a, sweepStaleRuntimeFiles as c, isUnderClaudeConfigMirror as i, writeRuntimeFileSecure as l, ensureClaudeConfigMirror as n, sweepStaleClaudeConfigMirrors as o, ensurePaths as r, sweepStalePeerAgentMdFiles as s, PATHS as t };
917
- //# sourceMappingURL=paths-CoFnpNZl.js.map
940
+ //# sourceMappingURL=paths-yJ97KlKp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths-yJ97KlKp.js","names":["_claudeConfigDirSuffix: string | undefined","CLAUDE_HOME_POLICY: ReadonlyMap<string, MirrorPolicy>","SHARED_TOPLEVEL_NAMES: ReadonlyArray<string>","existing: Record<string, unknown>","selectedSource: string | null","sourceCandidates: Array<string | null>","stat","merged: Record<string, unknown>","lst: import(\"node:fs\").Stats","raw: string","parsed: Record<string, unknown>","entries: Array<string>","stats: Awaited<ReturnType<typeof fs.lstat>>","existing: Awaited<ReturnType<typeof fs.lstat>> | null"],"sources":["../src/lib/paths.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport consola from \"consola\"\n\nfunction appDir(): string {\n return path.join(os.homedir(), \".local\", \"share\", \"github-router\")\n}\n\nexport const PATHS = {\n get APP_DIR() {\n return appDir()\n },\n get GITHUB_TOKEN_PATH() {\n return path.join(appDir(), \"github_token\")\n },\n get ERROR_LOG_PATH() {\n return path.join(appDir(), \"error.log\")\n },\n /**\n * Isolated CODEX_HOME for the spawned Codex CLI. Masks any cached\n * ChatGPT subscription login (openai/codex#2733 — cached login can\n * override OPENAI_API_KEY) so the proxy's dummy key is authoritative.\n */\n get CODEX_HOME() {\n return path.join(appDir(), \"codex-isolated\")\n },\n /**\n * Runtime tempfiles for the per-launch peer-MCP wiring (the\n * `--mcp-config` JSON and `--agents` JSON written before spawning\n * Claude Code). Mode 0o700 to match the security review's mandate;\n * cleaned on shutdown via the per-launch `cleanup()`, plus a\n * boot-time sweep of stale files (dead PIDs, >24h old).\n */\n get CLAUDE_RUNTIME_DIR() {\n return path.join(appDir(), \"runtime\")\n },\n /**\n * Router-owned CLAUDE_CONFIG_DIR. The spawned Claude Code (and any\n * teammates it spawns via the agent-teams primitive) reads its\n * config — including `.credentials.json` — from this dir. We\n * snapshot-copy the user's `~/.claude/` here at startup (excluding\n * `.credentials.json` and volatile state), then write our own\n * synthetic Console OAuth credential. The teammate-spawn allowlist\n * propagates `CLAUDE_CONFIG_DIR` to children, so teammates find the\n * synthetic credential and authenticate instead of falling into the\n * \"Not logged in · Run /login\" gate that would otherwise leave\n * them mute. See `ensureClaudeConfigMirror` below.\n *\n * Per-launch dir: `<appDir>/claude-config/<pid>-<8 hex>`. Two\n * concurrent `github-router claude` launches each get their own\n * isolated mirror, so per-launch state (synthetic credential,\n * snapshot copy of `~/.claude/`, future per-launch `.claude.json`\n * mutation with the peer-MCP entry) cannot cross-talk. The\n * per-launch suffix is cached on first access (see\n * `claudeConfigDirSuffix()`) so all callers within a single proxy\n * lifetime see the same value. Boot-time `sweepStaleClaudeConfigMirrors`\n * reaps mirrors from crashed prior PIDs.\n */\n get CLAUDE_CONFIG_DIR() {\n return path.join(appDir(), \"claude-config\", claudeConfigDirSuffix())\n },\n /**\n * Durable router-owned `bin/` for the LLM toolbelt — curated CLI\n * tools (rg/fd/jq/sd/sg/yq) materialized here and prepended to the\n * spawned agent's PATH. Intentionally OUTSIDE the per-launch\n * CLAUDE_CONFIG_DIR mirror (which is swept per launch): the toolbelt\n * is a cross-launch cache, like `last-update-check`.\n */\n get TOOLBELT_BIN_DIR() {\n return path.join(appDir(), \"bin\")\n },\n /**\n * Root of the github-router-managed ColBERT semantic-search sidecar\n * (the `--semantic-search` opt-in). Everything heavy that the\n * `colgrep` child needs is provisioned, SHA-verified, and stored here\n * by the router — never in the user's repo, never in colgrep's own\n * global `%APPDATA%\\colgrep` / `~/.config/colgrep`. Cross-launch cache\n * (like TOOLBELT_BIN_DIR), deliberately OUTSIDE the per-launch\n * CLAUDE_CONFIG_DIR mirror.\n *\n * Layout (see docs/research/colbert-sidecar-design.md §4):\n * colbert/\n * bin/colgrep[.exe] # provisioned binary\n * models/LateOn-Code-edge/<rev>/ # 5 model files\n * onnxruntime/1.23.0/cpu/<libname> # ORT dylib (ORT_DYLIB_PATH)\n * indices/ # === COLGREP_DATA_DIR ===\n * <project>-<xxh3(path|model)>/ # colgrep-owned per-(path,model)\n * .gh-router-meta/<hash>.json # router-owned sidecar metadata\n */\n get COLBERT_DIR() {\n return path.join(appDir(), \"colbert\")\n },\n get COLBERT_BIN_DIR() {\n return path.join(appDir(), \"colbert\", \"bin\")\n },\n get COLBERT_MODELS_DIR() {\n return path.join(appDir(), \"colbert\", \"models\")\n },\n get COLBERT_ORT_DIR() {\n return path.join(appDir(), \"colbert\", \"onnxruntime\")\n },\n /**\n * Value passed to colgrep as `COLGREP_DATA_DIR`. colgrep keys the\n * physical index dir by `xxh3(canonical_project_path | model)` under\n * here, so every distinct workspace gets its own index dir for free.\n * The router-owned freshness sidecar lives at\n * `<COLBERT_INDICES_DIR>/.gh-router-meta/<hash>.json`.\n */\n get COLBERT_INDICES_DIR() {\n return path.join(appDir(), \"colbert\", \"indices\")\n },\n get COLBERT_META_DIR() {\n return path.join(appDir(), \"colbert\", \"indices\", \".gh-router-meta\")\n },\n}\n\n/**\n * Per-launch suffix for `PATHS.CLAUDE_CONFIG_DIR`. Lazily generated on\n * first access and cached for the lifetime of the process so every\n * caller (env-var injection in `getClaudeCodeEnvVars`,\n * `ensureClaudeConfigMirror` provisioning, peer-agent `.md` writes\n * under `<dir>/agents/`, the shutdown cleanup) resolves the same path.\n *\n * Shape: `<pid>-<8 hex>`. The PID prefix is what\n * `sweepStaleClaudeConfigMirrors` keys off to drop orphans from\n * crashed prior sessions; the 8-hex random suffix prevents collision\n * if a future caller (tests, internal relaunch) ever clears the cache\n * within a single PID lifetime.\n *\n * NOT exported — every consumer should go through `PATHS.CLAUDE_CONFIG_DIR`\n * so the homedir-mock pattern used in the test suite keeps working.\n */\nlet _claudeConfigDirSuffix: string | undefined\nfunction claudeConfigDirSuffix(): string {\n if (_claudeConfigDirSuffix === undefined) {\n _claudeConfigDirSuffix = `${process.pid}-${randomBytes(4).toString(\"hex\")}`\n }\n return _claudeConfigDirSuffix\n}\n\n/**\n * Predicate: is `target` inside the current launch's `CLAUDE_CONFIG_DIR`?\n *\n * Used as a defence-in-depth safety guard by helpers that mutate files\n * under the mirror dir (e.g. `appendPeerAwarenessToMirroredClaudeMd`).\n * If `PATHS.CLAUDE_CONFIG_DIR` has somehow been pointed at the user's\n * real `~/.claude/` (configuration drift, future code regression), the\n * helper refuses to write — the proxy should never mutate user files.\n *\n * Pure-prefix `startsWith` is unsafe (`/a/foo` matches `/a/foobar`), so\n * this requires either equality or `<root><sep>...` strictly.\n * Synchronous because we don't need real-path resolution; symlink\n * refusal lives at the consuming helper.\n */\nexport function isUnderClaudeConfigMirror(target: string): boolean {\n const resolvedRoot = path.resolve(PATHS.CLAUDE_CONFIG_DIR)\n const resolvedTarget = path.resolve(target)\n if (resolvedTarget === resolvedRoot) return true\n return resolvedTarget.startsWith(resolvedRoot + path.sep)\n}\n\nexport async function ensurePaths(): Promise<void> {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n await fs.mkdir(PATHS.CODEX_HOME, { recursive: true })\n await fs.mkdir(PATHS.CLAUDE_RUNTIME_DIR, { recursive: true })\n // mkdir({recursive: true}) does NOT chmod an existing directory, so\n // explicitly tighten in case the dir was created by an older version.\n await chmodIfPossible(PATHS.CLAUDE_RUNTIME_DIR, 0o700)\n await ensureFile(PATHS.GITHUB_TOKEN_PATH)\n await sweepStaleRuntimeFiles().catch((err) => {\n consola.debug(\"Runtime sweep skipped:\", err)\n })\n // Sweep stale per-launch CLAUDE_CONFIG_DIR mirrors left behind by\n // crashed prior proxy sessions BEFORE peer-agent .md sweep, since\n // the .md sweep is scoped to THIS launch's mirror and the per-launch\n // dir sweep is the parent cleanup for the same orphan class.\n await sweepStaleClaudeConfigMirrors().catch((err) => {\n consola.debug(\"Per-launch claude-config sweep skipped:\", err)\n })\n // Phase 2.5: also sweep stale peer-* subagent .md files from this\n // launch's CLAUDE_CONFIG_DIR/agents/ (defense-in-depth — should be\n // a no-op since the per-launch dir didn't exist before this PID\n // started; keeps the safety net in case a future change ever shares\n // an agents/ dir across launches).\n await sweepStalePeerAgentMdFiles().catch((err) => {\n consola.debug(\"Peer-agent .md sweep skipped:\", err)\n })\n // Worker-agent boot-time PID+instance safety net. Walks the\n // worker-repos.json ledger and removes any worktree dir whose\n // <pid> is dead OR whose <instance> UUID doesn't match this proxy.\n // Catches SIGKILL/OOM/host-crash escapees from prior sessions.\n // Lazy-imported so the worker-agent module doesn't get loaded by\n // every consumer of `paths.ts`.\n await (async () => {\n const mod = await import(\"./worker-agent/lifecycle\")\n await mod.sweepStaleWorktreesAtBoot()\n })().catch((err) => {\n consola.debug(\"Worker worktree boot sweep skipped:\", err)\n })\n // ColBERT sidecar boot sweep: reclassify any `.gh-router-meta/*.json`\n // entry stuck in `building` with a dead `buildPid` → `failed`, so the\n // next semantic_search re-kicks a build instead of routing to a\n // never-finishing one. NEVER kills a PID from a prior boot. Lazy-\n // imported so paths.ts doesn't drag the colbert module into every\n // consumer.\n await (async () => {\n const mod = await import(\"./colbert/lifecycle\")\n await mod.sweepStaleColbertMetaAtBoot()\n })().catch((err) => {\n consola.debug(\"ColBERT meta boot sweep skipped:\", err)\n })\n}\n\n/**\n * Per-entry mirror policy. Every top-level entry under `~/.claude/` falls\n * into exactly one bucket; unlisted names default to `MIRRORED` so a future\n * Claude-Code-side addition flows through as a snapshot copy rather than\n * being silently lost.\n *\n * Three policies:\n *\n * - `ISOLATED` — not present in the mirror at all. The proxy owns its\n * own copy (synthetic `.credentials.json`, the `.github-router-managed`\n * marker) or the entry has no place in a proxy session\n * (`.credentials.json.lock`, `.oauth_refresh.lock` couple refresh loops\n * across sessions; `statsig/` is write-heavy and would constantly\n * re-copy; `cache/` and `logs/` are ephemeral; `paste-cache/` holds\n * sensitive clipboard extracts and shouldn't leak across sessions —\n * gemini-critic finding).\n *\n * - `SHARED` — symlink `<mirror>/<X>` → `~/.claude/<X>` so writes made\n * during the proxy session land in the user's real `~/.claude/` and\n * chat history is visible in both proxy and plain-`claude` sessions.\n * **Directories only.** Never use this for individual files: Claude\n * Code's atomic-write pattern (`fs.writeFile(temp); fs.rename(temp,\n * target)`) does NOT follow symlinks — a `rename` over the symlink\n * replaces it with a regular file, silently severing the connection\n * to `~/.claude/<X>`. Gemini-critic finding from the 3-lab review.\n *\n * - `MIRRORED` (default) — snapshot-copy with mtime skip. Use for static\n * or settings-shaped state where proxy-session writes should NOT flow\n * back to `~/.claude/` (e.g. `settings.json`, `.claude.json`,\n * `teams/`, `session-env/`) and for `agents/` — the proxy itself\n * writes per-launch `peer-<pid>-*.md` files into the mirror's `agents/`\n * and `sweepStalePeerAgentMdFiles` deletes them; a symlink would route\n * those writes/deletes into the user's real `~/.claude/agents/` and\n * destroy the user's own subagent files. **Hard regression test**:\n * `policyFor(\"agents\") === \"MIRRORED\"` is asserted in\n * `tests/lib-paths.test.ts` to prevent accidental reclassification.\n *\n * Sub-paths within MIRRORED dirs cascade recursively (existing behavior).\n */\ntype MirrorPolicy = \"ISOLATED\" | \"SHARED\" | \"MIRRORED\"\n\nconst CLAUDE_HOME_POLICY: ReadonlyMap<string, MirrorPolicy> = new Map<\n string,\n MirrorPolicy\n>([\n // ISOLATED\n [\".credentials.json\", \"ISOLATED\"],\n [\".credentials.json.lock\", \"ISOLATED\"],\n [\".oauth_refresh.lock\", \"ISOLATED\"],\n // Defense-in-depth: don't let a user-side file/symlink with the same\n // name as our marker collide with what we write. The marker write\n // logic also lstat-checks before writing (refuses if a non-regular\n // file exists at the path), but excluding it here removes the\n // attack vector entirely.\n [\".github-router-managed\", \"ISOLATED\"],\n [\"statsig\", \"ISOLATED\"],\n [\"cache\", \"ISOLATED\"],\n [\"logs\", \"ISOLATED\"],\n [\"paste-cache\", \"ISOLATED\"],\n [\"jobs\", \"ISOLATED\"],\n [\"daemon\", \"ISOLATED\"],\n [\"daemon.log\", \"ISOLATED\"],\n // SHARED — directories only (see policy doc above)\n [\"projects\", \"SHARED\"],\n [\"sessions\", \"SHARED\"],\n [\"tasks\", \"SHARED\"],\n [\"todos\", \"SHARED\"],\n [\"transcripts\", \"SHARED\"],\n [\"shell-snapshots\", \"SHARED\"],\n // The underscored variant is the historical exclude-list name; some\n // Claude Code versions may still use it. Classify SHARED so either\n // spelling resolves correctly.\n [\"shell_snapshots\", \"SHARED\"],\n [\"plans\", \"SHARED\"],\n [\"file-history\", \"SHARED\"],\n [\"backups\", \"SHARED\"],\n])\n\nfunction policyFor(name: string): MirrorPolicy {\n return CLAUDE_HOME_POLICY.get(name) ?? \"MIRRORED\"\n}\n\n/**\n * Test-only export: lets the test suite assert hard regression guards\n * such as `policyFor(\"agents\") === \"MIRRORED\"` (preventing accidental\n * reclassification that would let `sweepStalePeerAgentMdFiles` delete\n * files in the user's real `~/.claude/agents/`).\n */\nexport const __testing = { policyFor, ensureSharedSymlink }\n\n/**\n * Names with `SHARED` policy, materialized once for iteration in\n * `ensureClaudeConfigMirror`'s post-copy phase.\n */\nconst SHARED_TOPLEVEL_NAMES: ReadonlyArray<string> = Array.from(\n CLAUDE_HOME_POLICY.entries(),\n)\n .filter(([, kind]) => kind === \"SHARED\")\n .map(([name]) => name)\n\n/**\n * Marker file written into the router-owned CLAUDE_CONFIG_DIR so users\n * (and our own future sweeps) can identify that the dir is managed by\n * github-router. Content is informational only; no logic depends on\n * its presence.\n */\nconst MANAGED_MARKER_FILENAME = \".github-router-managed\"\n\n/**\n * Synthetic Console OAuth credential the router writes into its own\n * `CLAUDE_CONFIG_DIR/.credentials.json` so spawned Claude Code (and\n * any teammates it spawns) can authenticate without a real user\n * `/login`.\n *\n * Schema verified verbatim from `claude` v2.1.140 binary, function\n * `guH` (the credentials-save mutation). Fields:\n * - `accessToken` — sent as `Authorization: Bearer ...` to the\n * proxy. Proxy accepts any bearer (per CLAUDE.md \"doesn't enforce\n * auth\").\n * - `refreshToken` — only used by Claude Code's reactive refresh\n * path (function `nH8`), which fires on 401 from upstream. The\n * proxy maintains the no-401 invariant on the Anthropic-shape\n * boundary, so this is never invoked. Synthetic value is fine.\n * - `expiresAt` — far-future (2099-01-01 ms epoch). Sidesteps the\n * proactive refresh path (`R8H(expiresAt)` returns false).\n * - `scopes` — claude-ai-shaped so `tB(scopes)` returns true,\n * making `Hq()` true (full feature surface, not \"inference only\").\n * - `subscriptionType` — `\"max\"`. Pure client-side label\n * (`e7()` / `Zc_()` / `CZ1()`); no server validation since\n * `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` suppresses\n * subscription-validation calls. Picks the most-permissive gating.\n * - `rateLimitTier` — `\"default_claude_max_20x\"`. Paired with\n * `subscriptionType:\"max\"` this is the real Max-20x tier, so the\n * credential is internally consistent (vs the prior odd `max`+`null`).\n * Verified live (claude v2.1.158) call-sites are cosmetic billing /\n * upsell-suppression UI plus the `getPlanModeV2AgentCount` (`bGK`)\n * `max && 20x → 3` branch — which `CLAUDE_CODE_PLAN_V2_AGENT_COUNT`\n * (set to 7 in server-setup) already overrides, so this is\n * belt-and-suspenders for the natural code path. No client-side quota\n * enforcement keys off the tier (rate-limit UI reads server\n * `x-ratelimit-*` headers; the proxy holds the no-429 invariant).\n */\nconst SYNTHETIC_CREDENTIAL = {\n claudeAiOauth: {\n accessToken: \"github-router-synthetic\",\n refreshToken: \"github-router-synthetic\",\n expiresAt: 4_070_908_800_000,\n scopes: [\"user:inference\", \"user:profile\"],\n subscriptionType: \"max\",\n rateLimitTier: \"default_claude_max_20x\",\n clientId: \"github-router\",\n },\n} as const\n\n/**\n * Router-owned fields injected into `<CLAUDE_CONFIG_DIR>/.claude.json` so\n * Claude Code skips its first-launch onboarding wizard. The synthetic\n * `.credentials.json` covers the OAuth *token*, but on a fresh machine\n * the wizard runs anyway — and one of its steps is the browser-OAuth\n * \"Sign in to your Anthropic account\" prompt that the user sees today.\n *\n * Schema verified against `claude` v2.1.158 binary strings:\n *\n * - `hasCompletedOnboarding: true` — single load-bearing gate.\n * Binary code path: `if (!E_().hasCompletedOnboarding) { ... await\n * Onboarding.default(...) }`. Setting true causes the wizard\n * (including the OAuth login step) to be skipped entirely.\n *\n * - `bypassPermissionsModeAccepted: true` — skips the\n * `--dangerously-skip-permissions` trust disclaimer. Binary gate:\n * `K = Ip() || Boolean(E_().bypassPermissionsModeAccepted)`. The\n * proxy passes `--dangerously-skip-permissions` so `Ip()` typically\n * suffices, but pinning the field covers edge code paths (e.g.\n * `--bg --dangerously-skip-permissions`) that still consult it\n * directly and would otherwise throw \"requires accepting the\n * disclaimer first.\"\n *\n * - `oauthAccount` — nice-to-have for UI consistency. Claude Code\n * reads sub-fields with optional chaining (`oauthAccount?.accountUuid`,\n * `oauthAccount?.organizationUuid`, `oauthAccount?.organizationRole`)\n * so undefined doesn't trigger login — but populating gives any\n * \"logged in as X\" / status-line UI something deterministic and\n * obviously-synthetic to display. Values are intentionally\n * non-credible (`github-router@local`, all-zero UUIDs) so any\n * leak into logs is self-documenting, not impersonation.\n *\n * Merge semantics: the two boolean gates are **force-overridden** —\n * even an existing `false` is flipped to `true`. (A user logging out\n * via the Claude Code UI cannot defeat the proxy-session bypass.)\n * `oauthAccount` is overwritten only when the existing value isn't a\n * *structurally usable* one (presence-only would preserve `{}` or\n * `{foo:1}` shapes that defeat the fix); a real OAuth identity with\n * non-empty `accountUuid` and `organizationUuid` is preserved as-is.\n * Every OTHER top-level field the user brought in via the mirror walk\n * (settings, project history, user-side MCP entries, etc.) is preserved\n * verbatim. The mirror is a router-controlled view, not a faithful\n * copy — same trade-off the synthetic credential already makes.\n */\nconst SYNTHETIC_CLAUDE_JSON_FIELDS = {\n hasCompletedOnboarding: true,\n bypassPermissionsModeAccepted: true,\n oauthAccount: {\n accountUuid: \"00000000-0000-0000-0000-000000000000\",\n organizationUuid: \"00000000-0000-0000-0000-000000000000\",\n organizationRole: \"member\",\n emailAddress: \"github-router@local\",\n organizationName: \"github-router\",\n },\n} as const\n\n/**\n * Snapshot-copy the user's `~/.claude/` into the router-owned\n * CLAUDE_CONFIG_DIR (real files, not symlinks — symlinks don't isolate\n * writes), classifying each top-level entry per `CLAUDE_HOME_POLICY`:\n * ISOLATED entries are skipped, MIRRORED entries are copied, and\n * SHARED entries become directory symlinks back to `~/.claude/<X>` so\n * chat history (in `projects/<cwd-hash>/<session-uuid>.jsonl`) and\n * other durable user state flow between proxy and plain-`claude`\n * sessions. Then writes the synthetic `.credentials.json` so spawned\n * Claude Code (and teammates that inherit `CLAUDE_CONFIG_DIR`)\n * authenticate.\n *\n * Idempotent: only re-copies files whose source `mtime` is newer than\n * target; SHARED-symlink creation no-ops when the symlink already\n * points at the right target. Concurrent-safe: `mkdir({recursive:true})`\n * is idempotent; symlinks are created via atomic temp+rename so two\n * parallel github-router-claude startups can't race to EEXIST; the\n * credentials write uses temp-file + atomic rename so Claude Code's\n * `EZ1()` mtime watcher never sees a partial write.\n *\n * Walks with `lstat` (does NOT follow symlinks during traversal — a\n * symlink-into-`/` would otherwise let the walk escape). Symlink leaves\n * in the source tree are skipped during the MIRRORED copy walk (per the\n * symlink-confused-deputy security finding); SHARED symlinks are\n * created on the mirror side only, pointing at predetermined targets\n * inside the user's real `~/.claude/`.\n *\n * Caller is expected to invoke this after `ensurePaths()` and before\n * spawning Claude Code (`launchChild`). The mirror must exist before\n * the child reads it. Currently called from the `claude` subcommand\n * entry point only; `start` and `codex` subcommands don't need it.\n */\nexport async function ensureClaudeConfigMirror(opts: {\n realHome?: string\n} = {}): Promise<void> {\n const realHome = opts.realHome ?? os.homedir()\n const sourceDir = path.join(realHome, \".claude\")\n const targetDir = PATHS.CLAUDE_CONFIG_DIR\n\n // 1. Create our config dir (idempotent, mode 0o700)\n await fs.mkdir(targetDir, { recursive: true, mode: 0o700 })\n await chmodIfPossible(targetDir, 0o700)\n\n // 2. Snapshot-copy from ~/.claude if it exists. Only MIRRORED entries\n // flow through this walk; ISOLATED and SHARED entries are filtered\n // in `mirrorDirRecursive` and handled separately.\n let sourceExists = false\n try {\n const sourceStat = await fs.stat(sourceDir)\n sourceExists = sourceStat.isDirectory()\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot stat ${sourceDir}:`, err)\n }\n }\n if (sourceExists) {\n await mirrorDirRecursive(sourceDir, targetDir, \"\")\n }\n\n // 3. Always ensure agents/ exists (even if user has none) so the\n // peer-agent .md emission has a place to write. Empty dir is fine.\n // agents/ is MIRRORED, not SHARED — the proxy writes per-launch\n // `peer-<pid>-*.md` files here and `sweepStalePeerAgentMdFiles`\n // deletes them; routing those operations into the user's real\n // `~/.claude/agents/` would destroy their custom subagent files.\n await fs.mkdir(path.join(targetDir, \"agents\"), { recursive: true })\n\n // 4. Create symlinks for SHARED entries so chat history (and other\n // durable user state) is visible in both proxy and plain-`claude`.\n for (const name of SHARED_TOPLEVEL_NAMES) {\n await ensureSharedSymlink(name, sourceDir, targetDir).catch((err) => {\n consola.debug(\n `ensureClaudeConfigMirror: SHARED symlink for ${name} skipped:`,\n err,\n )\n })\n }\n\n // 5. Write synthetic .credentials.json (only if content differs)\n const credentialsPath = path.join(targetDir, \".credentials.json\")\n const desiredJson = JSON.stringify(SYNTHETIC_CREDENTIAL, null, 2)\n let needsWrite = true\n try {\n const existing = await fs.readFile(credentialsPath, \"utf8\")\n needsWrite = existing.trim() !== desiredJson.trim()\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot read existing credentials:`, err)\n }\n }\n if (needsWrite) {\n // Atomic temp-file + rename so EZ1()'s mtime watcher doesn't see\n // a partial write. wx flag ensures we don't clobber a concurrent\n // writer's tempfile.\n const tempPath = `${credentialsPath}.${process.pid}.tmp`\n try {\n await fs.writeFile(tempPath, desiredJson + \"\\n\", { mode: 0o600, flag: \"wx\" })\n await fs.rename(tempPath, credentialsPath)\n } catch (err) {\n // EEXIST on the tempfile means another concurrent startup is\n // mid-write. Best-effort: skip — the other writer will produce\n // identical content (deterministic constant blob).\n if ((err as NodeJS.ErrnoException).code === \"EEXIST\") {\n consola.debug(\n \"ensureClaudeConfigMirror: concurrent credentials-write detected, skipping\",\n )\n } else {\n await fs.unlink(tempPath).catch(() => {})\n throw err\n }\n }\n }\n await chmodIfPossible(credentialsPath, 0o600)\n\n // 6. Inject onboarding-skip fields into <CLAUDE_CONFIG_DIR>/.claude.json.\n // Runs AFTER the mirror walk (step 2) so a user's existing\n // .claude.json content (settings, projects, user-side mcpServers)\n // is preserved, and BEFORE `injectPeerMcpIntoMirror` (called later\n // from `src/claude.ts`) which read-merge-writes the same file and\n // will preserve our fields naturally.\n //\n // Without this step, on a brand-new machine where ~/.claude/.claude.json\n // doesn't exist, nothing lands in the mirror, Claude Code's\n // `hasCompletedOnboarding` gate defaults to falsy, and the\n // first-launch wizard runs — including the \"Sign in to your\n // Anthropic account\" OAuth step that defeats the synthetic\n // credential. See SYNTHETIC_CLAUDE_JSON_FIELDS for the gate\n // rationale + binary-verified field shape.\n await injectSyntheticClaudeJsonFields(targetDir, sourceDir, realHome)\n\n // 6a. Postcondition: verify the gate fields actually landed in the\n // mirror's .claude.json. The helper has several \"warn and\n // return\" branches (non-regular target, non-ENOENT read failure)\n // where falling through silently would let Claude Code's\n // first-launch wizard fire — the exact bug this whole\n // subsystem exists to prevent. Re-reading the file and\n // asserting the booleans converts every silent-fail path into a\n // fail-loud launch error, which `src/claude.ts` surfaces to the\n // user with `process.exit(1)`. Cheap (one extra readFile +\n // JSON.parse on a small file we just wrote) and gives the\n // no-silent-degradation invariant teeth even if a future\n // contributor adds a new bail-out branch.\n await assertOnboardingGateInjected(targetDir)\n\n // 7. Write/refresh marker file. Use lstat (not access) to detect\n // symlinks at the marker path — a previously-mirrored or\n // user-placed symlink could otherwise let our `fs.writeFile`\n // follow through to an arbitrary target. With the symlink-skip\n // policy in `mirrorDirRecursive` this is defense-in-depth, but\n // cheap and definitive.\n const markerPath = path.join(targetDir, MANAGED_MARKER_FILENAME)\n let markerExists = false\n try {\n const markerStat = await fs.lstat(markerPath)\n if (markerStat.isFile()) {\n markerExists = true\n } else {\n // Anything non-regular (symlink, dir, special file) is a red flag —\n // refuse to overwrite, log loudly. The user can investigate.\n consola.warn(\n `ensureClaudeConfigMirror: ${markerPath} exists but is not a regular file (mode=${markerStat.mode.toString(8)}); refusing to overwrite. Inspect and remove manually if safe.`,\n )\n markerExists = true\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot lstat marker:`, err)\n markerExists = true\n }\n }\n if (!markerExists) {\n const body = `Managed by github-router. Created ${new Date().toISOString()}. Safe to delete (will be recreated).\\n`\n // wx flag (O_CREAT | O_EXCL) refuses to clobber an existing\n // file or symlink (POSIX O_EXCL behavior) — additional protection\n // against the marker-symlink confused-deputy vector.\n await fs\n .writeFile(markerPath, body, { mode: 0o600, flag: \"wx\" })\n .catch((err) => {\n consola.debug(`ensureClaudeConfigMirror: marker write skipped:`, err)\n })\n }\n}\n\n/**\n * Read-merge-atomic-write the router-required onboarding-skip fields\n * (see `SYNTHETIC_CLAUDE_JSON_FIELDS`) into `<targetDir>/.claude.json`.\n *\n * Inputs:\n * - `targetDir` — the per-launch mirror dir we own.\n * - `sourceDir` — the user's `~/.claude/` (subdir-level legacy path).\n * - `realHome` — the user's `$HOME`. Used to locate the **canonical**\n * `~/.claude.json` (HOME level), which is what Claude Code's path\n * resolver `qG` actually reads when `CLAUDE_CONFIG_DIR` is unset:\n * `path.join(process.env.CLAUDE_CONFIG_DIR || homedir(),\n * \".claude.json\")`. The mirror walk operates on `~/.claude/` and\n * thus can only see the subdir-level `~/.claude/.claude.json`\n * (often a stale shadow); the canonical HOME-level file — where\n * `claude mcp add` writes and Claude Code reads on default\n * invocations — would otherwise be invisible to the proxy session.\n *\n * Behaviour:\n * - Mirror's `.claude.json` is a non-regular file (symlink, dir,\n * etc.) → log warn, refuse to touch. The postcondition check in\n * `ensureClaudeConfigMirror` then throws, blocking launch — the\n * user investigates the warn-flagged path rather than landing in\n * the OAuth wizard.\n * - Read existing content from the first VALID source, in priority\n * order:\n * (a) `~/.claude.json` HOME-level (canonical Claude Code path)\n * (b) The mirror file itself (snapshot-walk's product; only\n * consulted if it's a regular file per step 1)\n * (c) `~/.claude/.claude.json` subdir-level (legacy)\n * A candidate that's missing (ENOENT), too large (> 50 MiB cap),\n * or fails to parse as a non-empty object is **skipped** — the\n * loop falls through to the next candidate, so a malformed\n * higher-priority source can't drop valid lower-priority content\n * on the floor. HOME-level non-ENOENT read failures warn\n * (user-visible — corp perms / OneDrive). All other failures\n * debug-log. `fs.readFile` follows symlinks; reading user-owned\n * content into a per-launch user-owned dir is not a privilege\n * escalation (same uid).\n * - Merge: **force-override** all three synthetic fields\n * unconditionally — `hasCompletedOnboarding`,\n * `bypassPermissionsModeAccepted`, AND `oauthAccount`. A\n * within-proxy \"log out\" is impossible by design. `oauthAccount`\n * is overwritten even when the user has a real one, because\n * pairing the synthetic OAuth token (from `.credentials.json`)\n * with a real-user identity blob creates a split-brain auth\n * state that Claude Code's identity-vs-token cross-checks may\n * treat as session corruption (triggering re-login → defeats\n * the fix). The user's real identity remains intact in their\n * own `~/.claude.json`; only the proxy session sees synthetic.\n * `Object.assign(Object.create(null), existing)` instead of spread\n * defuses the `__proto__` prototype-pollution sink from JSON.parse.\n * - Idempotency: skip the write IFF the source we read FROM is the\n * mirror itself AND the current mirror content is byte-equivalent\n * to what we'd write. Bytes-on-disk comparison is robust against\n * filesystem mtime resolution (FAT/exFAT 2 s buckets) and the\n * source-presence-vs-mirror-presence conflation that broke\n * round-2's `needsWrite = !hadExisting` heuristic.\n *\n * Atomic write: tempfile (per-PID + 4-byte random suffix matching\n * `injectPeerMcpIntoMirror`'s pattern, so collision is effectively\n * impossible) + rename, mode 0o600. Pre-chmod the destination before\n * rename to defuse Windows read-only attribute issues. Any write\n * error throws to the caller — `src/claude.ts` fails the launch\n * loudly. Combined with the postcondition check, every silent-\n * degradation path is converted to a fail-loud launch error.\n */\nasync function injectSyntheticClaudeJsonFields(\n targetDir: string,\n sourceDir: string,\n realHome: string,\n): Promise<void> {\n const claudeJsonPath = path.join(targetDir, \".claude.json\")\n\n // 1. lstat the mirror path before touching it (defense-in-depth,\n // matching the marker-write at step 7). If something other than a\n // regular file occupies the slot, refuse — `fs.readFile` /\n // `chmodIfPossible` / `fs.rename` would otherwise follow / clobber\n // in surprising ways. The postcondition then throws.\n let mirrorIsRegularFile = false\n try {\n const mirrorStat = await fs.lstat(claudeJsonPath)\n if (mirrorStat.isFile()) {\n mirrorIsRegularFile = true\n } else {\n consola.warn(\n `ensureClaudeConfigMirror: ${claudeJsonPath} exists but is not a regular file (mode=${mirrorStat.mode.toString(8)}); refusing to inject synthetic fields. Inspect and remove manually if safe.`,\n )\n return\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.warn(\n `ensureClaudeConfigMirror: cannot lstat ${claudeJsonPath}; refusing to inject synthetic fields (overwriting an unreadable file would risk destroying user content):`,\n err,\n )\n return\n }\n // ENOENT: mirror file doesn't exist yet — fine; step 2 skips the\n // mirror candidate and step 4 creates a new file.\n }\n\n // 2. Read existing content from the first VALID source. Priority:\n // (a) `~/.claude.json` HOME-level (canonical Claude Code path)\n // (b) Mirror file (the snapshot-walk's product; only if regular)\n // (c) `~/.claude/.claude.json` subdir-level (legacy)\n // A candidate that's missing (ENOENT) or fails to parse as a\n // non-empty object is SKIPPED — the loop falls through to the\n // next candidate. This is the load-bearing fix for \"corrupt HOME\n // blocks valid subdir\": a zero-byte or malformed HOME file no\n // longer drops the user's good subdir content on the floor.\n // Size cap (`MAX_CLAUDE_JSON_BYTES`) skips absurdly large files\n // that would risk OOM. HOME read failures (non-ENOENT) warn\n // loudly — the canonical path failing is user-visible. Mirror /\n // subdir read failures debug-log and fall through.\n // `fs.readFile` follows symlinks; reading user-owned content\n // into a per-launch user-owned dir is not a privilege escalation.\n let existing: Record<string, unknown> = {}\n let selectedSource: string | null = null\n const homeLevelClaudeJson = path.join(realHome, \".claude.json\")\n const subdirClaudeJson = path.join(sourceDir, \".claude.json\")\n const sourceCandidates: Array<string | null> = [\n homeLevelClaudeJson,\n mirrorIsRegularFile ? claudeJsonPath : null,\n subdirClaudeJson,\n ]\n for (const sourceCandidate of sourceCandidates) {\n if (sourceCandidate === null) continue\n try {\n const stat = await fs.stat(sourceCandidate)\n if (stat.size > MAX_CLAUDE_JSON_BYTES) {\n consola.warn(\n `ensureClaudeConfigMirror: ${sourceCandidate} is ${stat.size} bytes (> ${MAX_CLAUDE_JSON_BYTES} cap); skipping this source to avoid OOM. Inspect for accidental log/state accumulation.`,\n )\n continue\n }\n const raw = await fs.readFile(sourceCandidate, \"utf8\")\n const parsed = parseExistingClaudeJson(raw, sourceCandidate)\n if (parsed === null) {\n // Malformed (parse failure or non-object). Skip; try next candidate.\n continue\n }\n existing = parsed\n selectedSource = sourceCandidate\n consola.debug(\n `ensureClaudeConfigMirror: read .claude.json content from ${sourceCandidate}`,\n )\n break\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === \"ENOENT\") {\n // Normal case — skip silently.\n continue\n }\n // Non-ENOENT read/stat failure. Severity depends on which source:\n // - HOME-level (canonical): warn (user-visible — corp perms,\n // OneDrive cloud-only, EACCES, etc.). Fall through, since\n // the gate-injection still needs to happen.\n // - Mirror / subdir: debug. Mirror failures used to abort,\n // but that locked out the still-valid subdir-level fallback;\n // we can always re-create the mirror from any valid source.\n if (sourceCandidate === homeLevelClaudeJson) {\n consola.warn(\n `ensureClaudeConfigMirror: cannot read canonical ${homeLevelClaudeJson}; falling back to other sources. User-scope MCPs added via 'claude mcp add' may be invisible to the proxy session:`,\n err,\n )\n } else {\n consola.debug(\n `ensureClaudeConfigMirror: cannot read source ${sourceCandidate}:`,\n err,\n )\n }\n // Fall through to next candidate. If all miss/fail, proceed\n // with synthetic-only content (existing stays {}).\n }\n }\n\n // 3. Build merged content. `Object.assign(Object.create(null), ...)`\n // instead of `{...existing}` avoids the `__proto__` prototype-\n // pollution sink (`JSON.parse('{\"__proto__\":{...}}')` sets the\n // object's prototype via defineProperty; spreading then triggers\n // the standard `__proto__` setter and mutates merged's prototype\n // chain. Cheap defense, prevents the entire class.\n //\n // All three gate fields are FORCE-OVERRIDDEN regardless of\n // existing value:\n // - `hasCompletedOnboarding` and `bypassPermissionsModeAccepted`\n // are the load-bearing gates we exist to set; a within-proxy\n // \"log out\" is impossible by design.\n // - `oauthAccount` is overwritten unconditionally with the\n // synthetic identity (NOT preserved even when real). Pairing\n // the synthetic OAuth token (from `.credentials.json`) with\n // a real-user `accountUuid` / `organizationUuid` creates a\n // split-brain auth state where Claude Code's identity-vs-\n // token cross-checks (binary `oauthAccount?.accountUuid`\n // consumers) may treat the mismatch as session corruption\n // and trigger re-login — the exact bug we're preventing.\n // The user's real identity remains intact in their own\n // `~/.claude.json`; only the proxy session sees synthetic.\n const merged: Record<string, unknown> = Object.assign(\n Object.create(null) as Record<string, unknown>,\n existing,\n )\n merged.hasCompletedOnboarding = SYNTHETIC_CLAUDE_JSON_FIELDS.hasCompletedOnboarding\n merged.bypassPermissionsModeAccepted = SYNTHETIC_CLAUDE_JSON_FIELDS.bypassPermissionsModeAccepted\n merged.oauthAccount = SYNTHETIC_CLAUDE_JSON_FIELDS.oauthAccount\n\n // 4. Decide if a write is needed. Idempotency contract: skip the\n // write IFF we read FROM the mirror itself AND the current mirror\n // content is byte-equivalent to what we'd write. Bytes-on-disk\n // comparison (vs. inferring from source state) is robust against:\n // - source-presence ≠ mirror-presence (a fully-onboarded HOME\n // file with no mirror yet must still trigger a mirror write)\n // - HOME content newer than mirror (HOME wins → mirror is\n // always rewritten when source was HOME)\n // - filesystem mtime resolution (FAT/exFAT 2s buckets)\n const desiredJson = JSON.stringify(merged, null, 2) + \"\\n\"\n if (selectedSource === claudeJsonPath) {\n try {\n const currentMirrorJson = await fs.readFile(claudeJsonPath, \"utf8\")\n if (currentMirrorJson === desiredJson) {\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n return\n }\n } catch {\n // Mirror read failed between step 2's success and now — fall\n // through to write. Worst case we rewrite identical content.\n }\n }\n\n // 5. Atomic temp + rename. Mode 0o600. Per-PID + 4-byte random\n // suffix matches `injectPeerMcpIntoMirror`'s pattern. Any write\n // error throws to the caller — `src/claude.ts` exits the launch\n // with a loud error message rather than silently degrading to\n // the OAuth wizard.\n //\n // Pre-chmod the destination before rename to defuse Windows\n // read-only attribute issues (FAT/exFAT volumes, corp-managed\n // perms): `fs.rename` over a read-only file can fail with EPERM\n // on Windows even though POSIX would silently succeed.\n //\n // Concurrent-racer tolerance: when multiple `ensureClaudeConfigMirror()`\n // calls run in parallel (e.g., from tests, or a future caller),\n // Windows `fs.rename` can fail with EBUSY/EPERM/EACCES if\n // another racer has the destination open or mid-write. The\n // write content is fully deterministic (same SYNTHETIC fields,\n // same force-override merge of the same source-priority list),\n // so a successful rename by ANY racer produces the same bytes\n // we'd produce. On rename failure, verify the on-disk content\n // matches `desiredJson` — if so, accept silently (another\n // racer won the race; the file is correct).\n const tempPath = `${claudeJsonPath}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`\n try {\n await fs.writeFile(tempPath, desiredJson, { mode: 0o600, flag: \"wx\" })\n if (mirrorIsRegularFile) {\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n }\n await fs.rename(tempPath, claudeJsonPath)\n } catch (err) {\n await fs.unlink(tempPath).catch(() => {})\n try {\n const observed = await fs.readFile(claudeJsonPath, \"utf8\")\n if (observed === desiredJson) {\n consola.debug(\n `ensureClaudeConfigMirror: rename failed but mirror already holds expected content (concurrent racer won the race):`,\n err,\n )\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n return\n }\n } catch {\n // Fall through to rethrow the original error.\n }\n throw err\n }\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n}\n\n/**\n * Size cap on candidate `.claude.json` reads in\n * `injectSyntheticClaudeJsonFields`. Real-world files are KB-MB scale;\n * if a `.claude.json` has grown to hundreds of MB (corruption, runaway\n * project history accumulation, accidental log redirect) reading it\n * blocks the event loop and risks OOM. 50 MiB is comfortably above\n * any legitimate Claude Code state and well below typical RSS budgets.\n */\nconst MAX_CLAUDE_JSON_BYTES = 50 * 1024 * 1024\n\n/**\n * Structural check for `oauthAccount`: must be a non-null, non-array\n * object whose `accountUuid` AND `organizationUuid` are non-empty\n * strings. Empty objects / partial blobs (missing UUIDs) do NOT count\n * as \"usable\" — preserving them would let Claude Code's various\n * \"logged in as X\" UIs read undefined sub-fields and (in some flows)\n * fall back to a re-login prompt that defeats the synthetic-credential\n * fix. Field names match the binary's optional-chain reads\n * (`oauthAccount?.accountUuid`, `oauthAccount?.organizationUuid`).\n */\nfunction isUsableOauthAccount(value: unknown): boolean {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false\n const obj = value as Record<string, unknown>\n const accountUuid = obj.accountUuid\n const organizationUuid = obj.organizationUuid\n return (\n typeof accountUuid === \"string\"\n && accountUuid.length > 0\n && typeof organizationUuid === \"string\"\n && organizationUuid.length > 0\n )\n}\n\n/**\n * Parse a JSON blob expected to be an object. Returns `null` on parse\n * failure or non-object/array value (so callers can SKIP this source\n * rather than treating an empty object as a successful read — a\n * round-1 finding from gemini-critic). Logs a warn so the user can\n * investigate. Used in `injectSyntheticClaudeJsonFields`'s source-\n * priority loop: a malformed HOME-level file falls through to the\n * mirror or subdir candidate instead of dropping valid lower-priority\n * content on the floor.\n */\nfunction parseExistingClaudeJson(\n raw: string,\n contextPath: string,\n): Record<string, unknown> | null {\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n consola.warn(\n `ensureClaudeConfigMirror: ${contextPath} parsed to non-object `\n + `(typeof=${typeof parsed}); skipping this source.`,\n )\n return null\n } catch (err) {\n consola.warn(\n `ensureClaudeConfigMirror: cannot parse ${contextPath} as JSON; `\n + `skipping this source:`,\n err,\n )\n return null\n }\n}\n\n/**\n * Postcondition for `ensureClaudeConfigMirror`: re-reads the mirror's\n * `.claude.json` and verifies that both boolean gate fields actually\n * landed. Converts every \"warn and return\" branch in\n * `injectSyntheticClaudeJsonFields` (non-regular target, non-ENOENT\n * read failure, postcondition lstat race) into a launch-failing\n * throw — `src/claude.ts:197`'s catch path turns it into a clear\n * `process.exit(1)` rather than silently spawning Claude Code into\n * the OAuth wizard. The whole point of this subsystem is preventing\n * that silent degradation; a warn that scrolls by in the launch log\n * is not enough.\n *\n * Cheap (one extra readFile + JSON.parse on a small file we just\n * wrote) and load-bearing: every future contributor who adds a new\n * bail-out branch in the helper above automatically gets fail-loud\n * for free instead of having to remember to thread the failure\n * upward by hand.\n */\nasync function assertOnboardingGateInjected(targetDir: string): Promise<void> {\n const claudeJsonPath = path.join(targetDir, \".claude.json\")\n\n // 1. lstat first — never follow a symlink. A planted symlink at the\n // mirror path pointing at an attacker-controlled file with valid\n // gate fields would otherwise pass a plain readFile check and let\n // the helper's \"non-regular target\" warn-and-return slip through.\n let lst: import(\"node:fs\").Stats\n try {\n lst = await fs.lstat(claudeJsonPath)\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — cannot lstat ${claudeJsonPath} after injection (synthetic onboarding-skip fields are required to prevent Claude Code's first-launch wizard, which would defeat the synthetic credential): ${(err as Error).message}`,\n )\n }\n if (!lst.isFile()) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} exists but is not a regular file (mode=${lst.mode.toString(8)}). Check the preceding warn-level log lines for the underlying cause; without a regular file holding the gate fields, Claude Code's first-launch wizard fires and defeats the synthetic credential.`,\n )\n }\n\n let raw: string\n try {\n raw = await fs.readFile(claudeJsonPath, \"utf8\")\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — cannot read ${claudeJsonPath} after injection: ${(err as Error).message}`,\n )\n }\n let parsed: Record<string, unknown>\n try {\n const json = JSON.parse(raw) as unknown\n if (!json || typeof json !== \"object\" || Array.isArray(json)) {\n throw new Error(`parsed to non-object (typeof=${typeof json})`)\n }\n parsed = json as Record<string, unknown>\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} is not a valid JSON object after injection: ${(err as Error).message}`,\n )\n }\n if (\n parsed.hasCompletedOnboarding !== true\n || parsed.bypassPermissionsModeAccepted !== true\n ) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} is missing required gate fields after injection (hasCompletedOnboarding=${String(parsed.hasCompletedOnboarding)}, bypassPermissionsModeAccepted=${String(parsed.bypassPermissionsModeAccepted)}). Check the preceding warn-level log lines for the underlying cause; without these fields Claude Code's first-launch wizard fires and defeats the synthetic credential.`,\n )\n }\n // Reuse `isUsableOauthAccount` so a future change to the structural\n // definition stays consistent across helper write-decision and\n // postcondition. The synthetic blob we always write satisfies this\n // by construction; a regression that drops oauthAccount during the\n // merge would otherwise slip past (per the round-2 codex/opus/\n // gemini-3-lab finding).\n if (!isUsableOauthAccount(parsed.oauthAccount)) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} has invalid oauthAccount after injection (got ${JSON.stringify(parsed.oauthAccount)}). The synthetic identity blob is required so Claude Code's identity-vs-token cross-checks don't trigger a re-login that defeats the synthetic credential.`,\n )\n }\n}\n\n/**\n * Recursive snapshot-copy helper for `ensureClaudeConfigMirror`. Walks\n * `sourceDir/relPath` and mirrors each entry into `targetDir/relPath`.\n * - Top-level entries are dispatched on `policyFor(name)`:\n * - `ISOLATED` → skipped entirely (no presence in mirror).\n * - `SHARED` → skipped from the copy walk; handled by\n * `ensureSharedSymlink` in the post-copy phase.\n * - `MIRRORED` → copied as today.\n * - Symlinks are skipped (not recreated) so the walk never follows out\n * of `sourceDir` and we don't reintroduce a confused-deputy vector.\n * - Files copy only if source mtime > target mtime (idempotent).\n */\nasync function mirrorDirRecursive(\n sourceDir: string,\n targetDir: string,\n relPath: string,\n): Promise<void> {\n const sourcePath = path.join(sourceDir, relPath)\n let entries: Array<string>\n try {\n entries = await fs.readdir(sourcePath)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n consola.debug(`mirrorDirRecursive: cannot readdir ${sourcePath}:`, err)\n return\n }\n for (const name of entries) {\n // Policy dispatch at top-level only. Sub-paths within MIRRORED\n // dirs always cascade as MIRRORED.\n if (relPath === \"\") {\n const policy = policyFor(name)\n if (policy === \"ISOLATED\" || policy === \"SHARED\") continue\n }\n const childRel = relPath === \"\" ? name : path.join(relPath, name)\n const childSource = path.join(sourceDir, childRel)\n const childTarget = path.join(targetDir, childRel)\n let stats: Awaited<ReturnType<typeof fs.lstat>>\n try {\n stats = await fs.lstat(childSource)\n } catch (err) {\n consola.debug(`mirrorDirRecursive: cannot lstat ${childSource}:`, err)\n continue\n }\n if (stats.isSymbolicLink()) {\n // Skip symlinks during mirror copy. gemini-critic security finding:\n // recreating user symlinks in our mirror creates a confused-deputy\n // vector — a previously prompt-injected process could place\n // `~/.claude/<X>` → `/some/sensitive/file`, our walker would mirror\n // it, and any subsequent write to `<mirror>/<X>` (by us or by\n // Claude Code) would follow the symlink and overwrite the target.\n // Snapshot-copy semantics make symlink preservation moot anyway:\n // a snapshot is a point-in-time content copy, and a symlink\n // recreated in the mirror points at exactly the same target as\n // the original would have — the user-side symlink is sufficient.\n // If a user has a legitimate need for a symlink to be visible\n // through the proxy session, they can create the equivalent\n // symlink in their `~/.claude/` directly and it'll be reachable\n // — they just won't see it in our mirror dir.\n consola.debug(`mirrorDirRecursive: skipping symlink ${childSource} (security policy)`)\n continue\n }\n if (stats.isDirectory()) {\n await fs.mkdir(childTarget, { recursive: true })\n await mirrorDirRecursive(sourceDir, targetDir, childRel)\n continue\n }\n if (stats.isFile()) {\n // mtime-based skip — only copy if source is newer than target.\n let needsCopy = true\n try {\n const targetStat = await fs.lstat(childTarget)\n if (targetStat.isFile() && targetStat.mtimeMs >= stats.mtimeMs) {\n needsCopy = false\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`mirrorDirRecursive: lstat target ${childTarget}:`, err)\n }\n }\n if (!needsCopy) continue\n try {\n await fs.copyFile(childSource, childTarget, fs.constants.COPYFILE_FICLONE)\n } catch (err) {\n consola.debug(`mirrorDirRecursive: copy ${childSource} → ${childTarget}:`, err)\n }\n continue\n }\n // Skip other inode types (sockets, devices, fifos) silently.\n }\n}\n\n/**\n * Create or refresh a directory symlink `<mirrorDir>/<name>` →\n * `<sourceDir>/<name>` (i.e. `~/.local/share/github-router/claude-config/<X>`\n * → `~/.claude/<X>`). Idempotent and concurrent-safe.\n *\n * Behavior depending on what's already at `<mirrorDir>/<name>`:\n * - Symlink with the correct target → no-op.\n * - Symlink with the wrong target → replace atomically.\n * - Empty real directory (legacy mirror leftover with no proxy-session\n * writes accumulated yet) → `rmdir` and replace with the symlink.\n * Safe by definition: `fs.rmdir` only succeeds on empty dirs (POSIX),\n * so there is nothing to lose. Smooths the upgrade path for users\n * whose legacy mirror dirs were never written to.\n * - Non-empty real directory or regular file → loud-warn and skip.\n * Auto-deleting would destroy proxy-session writes from the prior\n * version. The user is told the exact path and remediation.\n * - ENOENT → create symlink atomically.\n *\n * Atomic-creation: symlinks are first written at a unique side-path\n * (`<mirrorDir>/<name>.tmp.<pid>.<8 hex>`) and then `fs.rename()`d into\n * place. POSIX `rename` is atomic and replaces an existing symlink in\n * a single step, so two concurrent `github-router claude` startups can't\n * race to `EEXIST` — the loser's rename just overwrites the winner's\n * symlink with an identical one. Gemini-critic 3-lab-review finding.\n *\n * Pre-creates `~/.claude/<name>/` as a real directory if missing so\n * Claude Code's writes through the symlink don't fail with ENOENT.\n */\nasync function ensureSharedSymlink(\n name: string,\n sourceDir: string,\n mirrorDir: string,\n): Promise<void> {\n const sourcePath = path.join(sourceDir, name)\n const mirrorPath = path.join(mirrorDir, name)\n\n // 1. Ensure the source directory exists. Without this, Claude Code's\n // writes through the symlink (e.g. `projects/<hash>/foo.jsonl`)\n // fail with ENOENT on the parent dir.\n try {\n await fs.mkdir(sourcePath, { recursive: true })\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the symlink and rename catches below):\n // if the source dir cannot be created (e.g. a stray regular file\n // sitting at `~/.claude/projects`, perms blocking mkdir on a\n // corp-managed Windows box, OneDrive cloud-only reparse point),\n // ensureSharedSymlink returns without creating a junction. The\n // spawned Claude Code child then writes to the REAL `~/.claude`\n // while the proxy reads from the mirror — exactly the split-brain\n // pattern this whole function exists to prevent. Silent debug-log\n // hid this from us once already; warn so the user sees the cause.\n consola.warn(\n `ensureSharedSymlink(${name}): cannot mkdir source ${sourcePath}:`,\n err,\n )\n return\n }\n\n // 2. Inspect the mirror-side slot.\n let existing: Awaited<ReturnType<typeof fs.lstat>> | null = null\n try {\n existing = await fs.lstat(mirrorPath)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the other fs catches in this function):\n // ENOENT is the only expected-and-benign failure mode here\n // (slot doesn't exist yet — falls through to create). Any other\n // lstat failure (EACCES, ELOOP, EIO from a sketchy reparse\n // point) means we bail without creating the junction, which\n // silently leaves the proxy and child diverged. A visible warn\n // surfaces the root cause instead of a mysteriously missing\n // junction.\n consola.warn(\n `ensureSharedSymlink(${name}): cannot lstat ${mirrorPath}:`,\n err,\n )\n return\n }\n }\n\n if (existing?.isSymbolicLink()) {\n // Resolve both sides to their canonical absolute paths and compare.\n // We use `fs.realpath` rather than the raw `fs.readlink()` output\n // because Windows junctions resolve via readlink to `\\\\?\\`-prefixed\n // device-namespace paths (e.g. `\\\\?\\C:\\Users\\foo\\.claude\\projects`)\n // while we wrote the plain absolute `sourcePath` (e.g.\n // `C:\\Users\\foo\\.claude\\projects`) with `fs.symlink`. A literal\n // `===` on the raw readlink output never matched on Windows, so\n // the fast path silently failed and every startup tore down +\n // recreated all 9 SHARED junctions — masked locally because NTFS\n // File System Tunneling forges the creation timestamp for a name\n // deleted and recreated within 15 s (the per-startup churn was\n // real, the ctime-stable assertion was a false negative). The\n // realpath comparison canonicalizes both forms to the same string\n // on POSIX and Windows alike, and as a bonus handles drive-letter\n // casing / trailing-slash differences too. The extra two syscalls\n // per slot are negligible at proxy startup (runs once per launch).\n //\n // CRITICAL: sourceReal and currentReal are NOT treated symmetrically.\n // If `sourceReal` is null (we just mkdir'd it above, but realpath\n // failed — OneDrive cloud-only reparse point, EACCES on parent,\n // EXDEV mount oddity), we WARN AND RETURN rather than fall through.\n // Falling through would do unlink+symlink+rename with the same\n // failing realpath next launch — silent every-startup churn, the\n // exact bug class round-3 G2 fixed in a different code path.\n // `currentReal === null` is benign (broken/wrong slot — replace).\n const sourceReal = await fs.realpath(sourcePath).catch(() => null)\n if (sourceReal === null) {\n consola.warn(\n `ensureSharedSymlink(${name}): cannot resolve source ${sourcePath} ` +\n `— skipping junction creation to avoid silent every-startup churn. ` +\n `Inspect the source dir's permissions / OneDrive sync state and re-launch.`,\n )\n return\n }\n const currentReal = await fs.realpath(mirrorPath).catch(() => null)\n if (currentReal !== null && currentReal === sourceReal) {\n return\n }\n // Wrong target (or unresolvable mirror) — fall through to the\n // atomic-rename replace path.\n } else if (existing?.isDirectory()) {\n // Legacy real directory at the slot. Try `fs.rmdir` — on POSIX it\n // succeeds ONLY if the directory is empty, so there's nothing to\n // lose. If it's non-empty (ENOTEMPTY) or any other failure occurs,\n // fall back to the warn-and-skip path so we never auto-clobber\n // user data.\n try {\n await fs.rmdir(mirrorPath)\n // Empty dir reaped — fall through to the atomic-rename create path.\n } catch (err) {\n consola.warn(\n `ensureClaudeConfigMirror: ${mirrorPath} is a non-empty real directory ` +\n `from an older github-router version; refusing to clobber. ` +\n `If you want chat-history continuity for \"${name}\", move its ` +\n `contents into ${sourcePath}/ then delete ${mirrorPath}; the ` +\n `mirror will create a symlink (junction on Windows) on next launch. ` +\n `(rmdir error: ${(err as NodeJS.ErrnoException).code ?? \"unknown\"})`,\n )\n return\n }\n } else if (existing) {\n // Regular file (or special inode like a socket) — never auto-clobber.\n consola.warn(\n `ensureClaudeConfigMirror: ${mirrorPath} is a regular file at a ` +\n `SHARED symlink slot; refusing to clobber. Inspect and remove ` +\n `manually if safe; the mirror will create a symlink on next launch.`,\n )\n return\n }\n\n // 3. Atomic-rename creation: symlink to a unique temp path, then\n // rename over the slot. `fs.rename` replaces existing symlinks\n // atomically on POSIX and is safe against concurrent racers.\n // On Windows, MoveFileEx with MOVEFILE_REPLACE_EXISTING does NOT\n // replace an existing directory or junction destination\n // (npm/cli#9021), so when the slot already holds a wrong-target\n // junction we must explicitly unlink it first. The sub-millisecond\n // window of no-link is acceptable: ensureClaudeConfigMirror is\n // idempotent under concurrency and only runs at proxy startup,\n // before any spawned Claude Code child has been launched.\n const tempPath = `${mirrorPath}.tmp.${process.pid}.${randomBytes(4).toString(\"hex\")}`\n try {\n await fs.symlink(\n sourcePath,\n tempPath,\n process.platform === \"win32\" ? \"junction\" : \"dir\",\n )\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\" rule:\n // the rule applies to ALL fs catches in this function, not just the\n // rename one. The temp path is per-pid + 8-hex random so EEXIST is\n // essentially impossible — any failure here (EPERM on Windows\n // without DevMode, EXDEV cross-volume, ENOSPC, …) is a real\n // operational problem the user needs to see.\n consola.warn(\n `ensureSharedSymlink(${name}): symlink ${tempPath} failed:`,\n err,\n )\n return\n }\n if (process.platform === \"win32\" && existing?.isSymbolicLink()) {\n // Windows-only: clear the wrong-target junction so the rename\n // below can land. Best-effort — if a concurrent racer already\n // unlinked it, the rename succeeds as a CREATE; if a concurrent\n // racer already replaced it with a fresh junction, the rename\n // hits the catch below and we surface a warn.\n await fs.unlink(mirrorPath).catch(() => {})\n }\n try {\n await fs.rename(tempPath, mirrorPath)\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the fs.symlink catch above): a silent\n // debug log here previously hid the Windows rename-replace bug\n // (junction-over-junction MoveFileEx EPERM). Post-fix, rename\n // failures should be rare and visible.\n consola.warn(\n `ensureSharedSymlink(${name}): rename ${tempPath} → ${mirrorPath} failed:`,\n err,\n )\n await fs.unlink(tempPath).catch(() => {})\n }\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath, fs.constants.W_OK)\n } catch {\n await fs.writeFile(filePath, \"\")\n await fs.chmod(filePath, 0o600)\n }\n}\n\nasync function chmodIfPossible(target: string, mode: number): Promise<void> {\n if (process.platform === \"win32\") return // Windows chmod is no-op-ish\n try {\n await fs.chmod(target, mode)\n } catch (err) {\n consola.debug(`chmod ${target} ${mode.toString(8)} failed:`, err)\n }\n}\n\n/**\n * Write a runtime tempfile securely.\n *\n * - Mode `0o600` so other local users (multi-tenant boxes, shared\n * dev containers) can't read the per-launch nonce or runtime URL.\n * - `flag: \"wx\"` (O_CREAT | O_EXCL | O_WRONLY) refuses to overwrite\n * an existing path. POSIX open(2) with O_EXCL also rejects\n * pre-placed symlinks, killing the symlink-clobber attack vector.\n * - The caller's responsibility to pick a path NOT yet in use.\n * We intentionally do NOT pre-unlink: an `lstat` + `unlink` +\n * `open(O_EXCL)` sequence still has a TOCTOU window where an\n * attacker can drop a symlink between unlink and open. Letting\n * `wx` fail is the safer behavior — surfaces the conflict\n * instead of silently following.\n */\nexport async function writeRuntimeFileSecure(\n filePath: string,\n content: string,\n): Promise<void> {\n await fs.writeFile(filePath, content, { mode: 0o600, flag: \"wx\" })\n}\n\n/**\n * Sweep stale runtime tempfiles. Removes files whose embedded PID is no\n * longer a live process. A proxy crash (`kill -9`, OS reboot) leaves\n * orphans that would otherwise accumulate forever — and worse, a stale\n * config pointing at a now-recycled port could route MCP traffic to\n * whatever process bound that port next.\n *\n * Naming convention: `peer-mcp-<pid>.json` and `peer-agents-<pid>.json`.\n * Files not matching either pattern are left alone — this directory\n * is shared with future runtime artifacts.\n *\n * We deliberately do NOT age-prune files whose PID is alive. A\n * legitimately long-running proxy can have a tempfile older than any\n * arbitrary threshold; deleting it out from under the live process\n * breaks the spawned Claude Code child's MCP/agent wiring with no clean\n * recovery. PID-wraparound risk is mitigated by (a) PID reuse on Linux\n * being slow under typical loads, and (b) the file is only consulted by\n * github-router itself — an unrelated process that inherits the PID\n * never reads it.\n */\nexport async function sweepStaleRuntimeFiles(): Promise<void> {\n const dir = PATHS.CLAUDE_RUNTIME_DIR\n let entries: Array<string>\n try {\n entries = await fs.readdir(dir)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n\n for (const name of entries) {\n // Match both legacy `peer-mcp-<pid>.json` and current\n // `peer-mcp-<pid>-<rand>.json` filenames so we can clean up either.\n const match = /^peer-(?:mcp|agents)-(\\d+)(?:-[0-9a-f]+)?\\.json$/.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n const filePath = path.join(dir, name)\n\n if (isPidAlive(pid)) continue\n\n await fs.unlink(filePath).catch(() => {\n // already gone or unreadable, fine\n })\n }\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n // signal 0 = check existence without delivering a signal. EPERM\n // means the process exists but we can't signal it (which is still\n // \"alive\" for our purposes); ESRCH means it's gone.\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Sweep stale peer-* subagent .md files from the router-owned\n * `CLAUDE_CONFIG_DIR/agents/`. Phase 2.5 writes one .md per peer agent\n * into Claude Code's agents directory (now our config dir's `agents/`\n * subdir, since `getClaudeCodeEnvVars` points `CLAUDE_CONFIG_DIR` at\n * `PATHS.CLAUDE_CONFIG_DIR`) so they appear in Claude Code's Task\n * `subagent_type` enum. Files are named `peer-<pid>-<rand>-<agentName>.md`\n * so this sweep can drop orphans from crashed prior proxy sessions\n * without touching the user's own .md files (which were copied into\n * the same dir during `ensureClaudeConfigMirror`).\n *\n * Same liveness rule as `sweepStaleRuntimeFiles`: only delete when the\n * file's embedded PID is no longer alive. Live PIDs keep their files —\n * a long-running proxy doesn't lose its agent registrations.\n *\n * Regex tightening (Phase 2.6, codex-critic + gemini-critic 2-lab finding):\n * the original sweep regex `^peer-(\\d+)(?:-[0-9a-f]+)?-.+\\.md$` was too\n * permissive — a user-authored `peer-12345-meeting-notes.md` matches\n * (`12345` = \"PID\", `-meeting-notes` = trailing `.+`) and would be\n * silently unlinked when 12345 happens to be a dead PID (overwhelmingly\n * likely). Tightened to require BOTH the 8-hex-char random suffix AND\n * an exact-match persona name suffix, eliminating the risk for any\n * realistic user filename.\n */\nexport async function sweepStalePeerAgentMdFiles(): Promise<void> {\n const dir = path.join(PATHS.CLAUDE_CONFIG_DIR, \"agents\")\n let entries: Array<string>\n try {\n entries = await fs.readdir(dir)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n for (const name of entries) {\n const match = PEER_AGENT_MD_FILENAME.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n if (isPidAlive(pid)) continue\n await fs.unlink(path.join(dir, name)).catch(() => {\n // already gone or unreadable, fine\n })\n }\n}\n\n/**\n * Strict regex matching only files this proxy writes:\n * peer-<pid>-<8 hex>-<exact persona/coordinator name>.md\n * The persona-name allowlist is the load-bearing protection against\n * deleting user files. Update this list whenever a new persona is added\n * to `PERSONAS_READ` / `PERSONAS_WRITE` in `peer-mcp-personas.ts` or a\n * new coordinator-style agent is added in `codex-mcp-config.ts`.\n */\nconst PEER_AGENT_MD_FILENAME =\n /^peer-(\\d+)-[0-9a-f]{8}-(?:codex-critic|codex-reviewer|gemini-critic|gemini-reviewer|opus-critic|codex-implementer|peer-review-coordinator)\\.md$/\n\n/**\n * Strict regex matching only per-launch claude-config mirror dirs this\n * proxy creates: `<pid>-<8 hex>`. Anchored to the entire entry name so\n * user-authored siblings under `<appDir>/claude-config/` (if any) are\n * untouchable. The PID prefix is what `sweepStaleClaudeConfigMirrors`\n * keys off; the 8-hex random suffix matches `randomBytes(4)` exactly\n * (no `?` — files created by a different shape are not ours).\n */\nconst CLAUDE_CONFIG_MIRROR_DIR = /^(\\d+)-[0-9a-f]{8}$/\n\n/**\n * Sweep stale per-launch CLAUDE_CONFIG_DIR mirrors left behind by\n * crashed prior proxy sessions. Symmetric to `sweepStalePeerAgentMdFiles`\n * — same liveness rule (only delete when the embedded PID is dead),\n * same strict regex (the dir-name allowlist is the load-bearing\n * protection against deleting user-authored siblings).\n *\n * Scans `<appDir>/claude-config/` (the parent of the per-launch dirs).\n * Each entry whose name matches `<pid>-<8 hex>` AND whose PID is no\n * longer alive is removed recursively. `fs.rm({recursive: true})`\n * walks the tree calling `unlink` on symlinks/junctions rather than\n * following them, so the SHARED junctions back to `~/.claude/<X>`\n * are removed without touching their targets.\n *\n * Tolerates missing parent dir (first-ever launch, or user wiped it).\n */\nexport async function sweepStaleClaudeConfigMirrors(): Promise<void> {\n const parent = path.join(appDir(), \"claude-config\")\n let entries: Array<string>\n try {\n entries = await fs.readdir(parent)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n for (const name of entries) {\n const match = CLAUDE_CONFIG_MIRROR_DIR.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n if (isPidAlive(pid)) continue\n await fs\n .rm(path.join(parent, name), { recursive: true, force: true })\n .catch((err) => {\n // Best-effort: stale-dir cleanup must never block startup.\n // Common failure modes (worth surviving silently): an EBUSY/EPERM\n // on Windows if a leftover handle is still open, or a stray\n // root-owned file inside the dir from a previous run with\n // different permissions.\n consola.debug(\n `sweepStaleClaudeConfigMirrors: cannot rm ${name}:`,\n err,\n )\n })\n }\n}\n\n/**\n * Remove THIS launch's per-launch CLAUDE_CONFIG_DIR on shutdown.\n * Best-effort: a failure here must not block process exit (the caller\n * wraps this in a `.catch`-equivalent via `launchChild`'s onShutdown\n * try/catch). Symmetric to `writePeerMcpRuntimeFiles`'s `cleanup()`:\n * we own this dir for the lifetime of the proxy, so removing it on\n * normal shutdown is correct; the boot-time sweep handles the\n * abnormal-exit case.\n *\n * `fs.rm({recursive: true})` removes SHARED junctions via unlink\n * (does NOT follow them into the user's real `~/.claude/<X>`).\n */\nexport async function removeOwnClaudeConfigMirror(): Promise<void> {\n const dir = PATHS.CLAUDE_CONFIG_DIR\n await fs.rm(dir, { recursive: true, force: true }).catch((err) => {\n consola.debug(`removeOwnClaudeConfigMirror: rm ${dir} skipped:`, err)\n })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github-router",
3
- "version": "0.3.71",
3
+ "version": "0.3.73",
4
4
  "license": "MIT",
5
5
  "description": "A reverse proxy that exposes GitHub Copilot as OpenAI and Anthropic compatible API endpoints.",
6
6
  "keywords": [
@@ -1 +0,0 @@
1
- {"version":3,"file":"paths-CoFnpNZl.js","names":["_claudeConfigDirSuffix: string | undefined","CLAUDE_HOME_POLICY: ReadonlyMap<string, MirrorPolicy>","SHARED_TOPLEVEL_NAMES: ReadonlyArray<string>","existing: Record<string, unknown>","selectedSource: string | null","sourceCandidates: Array<string | null>","stat","merged: Record<string, unknown>","lst: import(\"node:fs\").Stats","raw: string","parsed: Record<string, unknown>","entries: Array<string>","stats: Awaited<ReturnType<typeof fs.lstat>>","existing: Awaited<ReturnType<typeof fs.lstat>> | null"],"sources":["../src/lib/paths.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\n\nimport consola from \"consola\"\n\nfunction appDir(): string {\n return path.join(os.homedir(), \".local\", \"share\", \"github-router\")\n}\n\nexport const PATHS = {\n get APP_DIR() {\n return appDir()\n },\n get GITHUB_TOKEN_PATH() {\n return path.join(appDir(), \"github_token\")\n },\n get ERROR_LOG_PATH() {\n return path.join(appDir(), \"error.log\")\n },\n /**\n * Isolated CODEX_HOME for the spawned Codex CLI. Masks any cached\n * ChatGPT subscription login (openai/codex#2733 — cached login can\n * override OPENAI_API_KEY) so the proxy's dummy key is authoritative.\n */\n get CODEX_HOME() {\n return path.join(appDir(), \"codex-isolated\")\n },\n /**\n * Runtime tempfiles for the per-launch peer-MCP wiring (the\n * `--mcp-config` JSON and `--agents` JSON written before spawning\n * Claude Code). Mode 0o700 to match the security review's mandate;\n * cleaned on shutdown via the per-launch `cleanup()`, plus a\n * boot-time sweep of stale files (dead PIDs, >24h old).\n */\n get CLAUDE_RUNTIME_DIR() {\n return path.join(appDir(), \"runtime\")\n },\n /**\n * Router-owned CLAUDE_CONFIG_DIR. The spawned Claude Code (and any\n * teammates it spawns via the agent-teams primitive) reads its\n * config — including `.credentials.json` — from this dir. We\n * snapshot-copy the user's `~/.claude/` here at startup (excluding\n * `.credentials.json` and volatile state), then write our own\n * synthetic Console OAuth credential. The teammate-spawn allowlist\n * propagates `CLAUDE_CONFIG_DIR` to children, so teammates find the\n * synthetic credential and authenticate instead of falling into the\n * \"Not logged in · Run /login\" gate that would otherwise leave\n * them mute. See `ensureClaudeConfigMirror` below.\n *\n * Per-launch dir: `<appDir>/claude-config/<pid>-<8 hex>`. Two\n * concurrent `github-router claude` launches each get their own\n * isolated mirror, so per-launch state (synthetic credential,\n * snapshot copy of `~/.claude/`, future per-launch `.claude.json`\n * mutation with the peer-MCP entry) cannot cross-talk. The\n * per-launch suffix is cached on first access (see\n * `claudeConfigDirSuffix()`) so all callers within a single proxy\n * lifetime see the same value. Boot-time `sweepStaleClaudeConfigMirrors`\n * reaps mirrors from crashed prior PIDs.\n */\n get CLAUDE_CONFIG_DIR() {\n return path.join(appDir(), \"claude-config\", claudeConfigDirSuffix())\n },\n /**\n * Durable router-owned `bin/` for the LLM toolbelt — curated CLI\n * tools (rg/fd/jq/sd/sg/yq) materialized here and prepended to the\n * spawned agent's PATH. Intentionally OUTSIDE the per-launch\n * CLAUDE_CONFIG_DIR mirror (which is swept per launch): the toolbelt\n * is a cross-launch cache, like `last-update-check`.\n */\n get TOOLBELT_BIN_DIR() {\n return path.join(appDir(), \"bin\")\n },\n}\n\n/**\n * Per-launch suffix for `PATHS.CLAUDE_CONFIG_DIR`. Lazily generated on\n * first access and cached for the lifetime of the process so every\n * caller (env-var injection in `getClaudeCodeEnvVars`,\n * `ensureClaudeConfigMirror` provisioning, peer-agent `.md` writes\n * under `<dir>/agents/`, the shutdown cleanup) resolves the same path.\n *\n * Shape: `<pid>-<8 hex>`. The PID prefix is what\n * `sweepStaleClaudeConfigMirrors` keys off to drop orphans from\n * crashed prior sessions; the 8-hex random suffix prevents collision\n * if a future caller (tests, internal relaunch) ever clears the cache\n * within a single PID lifetime.\n *\n * NOT exported — every consumer should go through `PATHS.CLAUDE_CONFIG_DIR`\n * so the homedir-mock pattern used in the test suite keeps working.\n */\nlet _claudeConfigDirSuffix: string | undefined\nfunction claudeConfigDirSuffix(): string {\n if (_claudeConfigDirSuffix === undefined) {\n _claudeConfigDirSuffix = `${process.pid}-${randomBytes(4).toString(\"hex\")}`\n }\n return _claudeConfigDirSuffix\n}\n\n/**\n * Predicate: is `target` inside the current launch's `CLAUDE_CONFIG_DIR`?\n *\n * Used as a defence-in-depth safety guard by helpers that mutate files\n * under the mirror dir (e.g. `appendPeerAwarenessToMirroredClaudeMd`).\n * If `PATHS.CLAUDE_CONFIG_DIR` has somehow been pointed at the user's\n * real `~/.claude/` (configuration drift, future code regression), the\n * helper refuses to write — the proxy should never mutate user files.\n *\n * Pure-prefix `startsWith` is unsafe (`/a/foo` matches `/a/foobar`), so\n * this requires either equality or `<root><sep>...` strictly.\n * Synchronous because we don't need real-path resolution; symlink\n * refusal lives at the consuming helper.\n */\nexport function isUnderClaudeConfigMirror(target: string): boolean {\n const resolvedRoot = path.resolve(PATHS.CLAUDE_CONFIG_DIR)\n const resolvedTarget = path.resolve(target)\n if (resolvedTarget === resolvedRoot) return true\n return resolvedTarget.startsWith(resolvedRoot + path.sep)\n}\n\nexport async function ensurePaths(): Promise<void> {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n await fs.mkdir(PATHS.CODEX_HOME, { recursive: true })\n await fs.mkdir(PATHS.CLAUDE_RUNTIME_DIR, { recursive: true })\n // mkdir({recursive: true}) does NOT chmod an existing directory, so\n // explicitly tighten in case the dir was created by an older version.\n await chmodIfPossible(PATHS.CLAUDE_RUNTIME_DIR, 0o700)\n await ensureFile(PATHS.GITHUB_TOKEN_PATH)\n await sweepStaleRuntimeFiles().catch((err) => {\n consola.debug(\"Runtime sweep skipped:\", err)\n })\n // Sweep stale per-launch CLAUDE_CONFIG_DIR mirrors left behind by\n // crashed prior proxy sessions BEFORE peer-agent .md sweep, since\n // the .md sweep is scoped to THIS launch's mirror and the per-launch\n // dir sweep is the parent cleanup for the same orphan class.\n await sweepStaleClaudeConfigMirrors().catch((err) => {\n consola.debug(\"Per-launch claude-config sweep skipped:\", err)\n })\n // Phase 2.5: also sweep stale peer-* subagent .md files from this\n // launch's CLAUDE_CONFIG_DIR/agents/ (defense-in-depth — should be\n // a no-op since the per-launch dir didn't exist before this PID\n // started; keeps the safety net in case a future change ever shares\n // an agents/ dir across launches).\n await sweepStalePeerAgentMdFiles().catch((err) => {\n consola.debug(\"Peer-agent .md sweep skipped:\", err)\n })\n // Worker-agent boot-time PID+instance safety net. Walks the\n // worker-repos.json ledger and removes any worktree dir whose\n // <pid> is dead OR whose <instance> UUID doesn't match this proxy.\n // Catches SIGKILL/OOM/host-crash escapees from prior sessions.\n // Lazy-imported so the worker-agent module doesn't get loaded by\n // every consumer of `paths.ts`.\n await (async () => {\n const mod = await import(\"./worker-agent/lifecycle\")\n await mod.sweepStaleWorktreesAtBoot()\n })().catch((err) => {\n consola.debug(\"Worker worktree boot sweep skipped:\", err)\n })\n}\n\n/**\n * Per-entry mirror policy. Every top-level entry under `~/.claude/` falls\n * into exactly one bucket; unlisted names default to `MIRRORED` so a future\n * Claude-Code-side addition flows through as a snapshot copy rather than\n * being silently lost.\n *\n * Three policies:\n *\n * - `ISOLATED` — not present in the mirror at all. The proxy owns its\n * own copy (synthetic `.credentials.json`, the `.github-router-managed`\n * marker) or the entry has no place in a proxy session\n * (`.credentials.json.lock`, `.oauth_refresh.lock` couple refresh loops\n * across sessions; `statsig/` is write-heavy and would constantly\n * re-copy; `cache/` and `logs/` are ephemeral; `paste-cache/` holds\n * sensitive clipboard extracts and shouldn't leak across sessions —\n * gemini-critic finding).\n *\n * - `SHARED` — symlink `<mirror>/<X>` → `~/.claude/<X>` so writes made\n * during the proxy session land in the user's real `~/.claude/` and\n * chat history is visible in both proxy and plain-`claude` sessions.\n * **Directories only.** Never use this for individual files: Claude\n * Code's atomic-write pattern (`fs.writeFile(temp); fs.rename(temp,\n * target)`) does NOT follow symlinks — a `rename` over the symlink\n * replaces it with a regular file, silently severing the connection\n * to `~/.claude/<X>`. Gemini-critic finding from the 3-lab review.\n *\n * - `MIRRORED` (default) — snapshot-copy with mtime skip. Use for static\n * or settings-shaped state where proxy-session writes should NOT flow\n * back to `~/.claude/` (e.g. `settings.json`, `.claude.json`,\n * `teams/`, `session-env/`) and for `agents/` — the proxy itself\n * writes per-launch `peer-<pid>-*.md` files into the mirror's `agents/`\n * and `sweepStalePeerAgentMdFiles` deletes them; a symlink would route\n * those writes/deletes into the user's real `~/.claude/agents/` and\n * destroy the user's own subagent files. **Hard regression test**:\n * `policyFor(\"agents\") === \"MIRRORED\"` is asserted in\n * `tests/lib-paths.test.ts` to prevent accidental reclassification.\n *\n * Sub-paths within MIRRORED dirs cascade recursively (existing behavior).\n */\ntype MirrorPolicy = \"ISOLATED\" | \"SHARED\" | \"MIRRORED\"\n\nconst CLAUDE_HOME_POLICY: ReadonlyMap<string, MirrorPolicy> = new Map<\n string,\n MirrorPolicy\n>([\n // ISOLATED\n [\".credentials.json\", \"ISOLATED\"],\n [\".credentials.json.lock\", \"ISOLATED\"],\n [\".oauth_refresh.lock\", \"ISOLATED\"],\n // Defense-in-depth: don't let a user-side file/symlink with the same\n // name as our marker collide with what we write. The marker write\n // logic also lstat-checks before writing (refuses if a non-regular\n // file exists at the path), but excluding it here removes the\n // attack vector entirely.\n [\".github-router-managed\", \"ISOLATED\"],\n [\"statsig\", \"ISOLATED\"],\n [\"cache\", \"ISOLATED\"],\n [\"logs\", \"ISOLATED\"],\n [\"paste-cache\", \"ISOLATED\"],\n [\"jobs\", \"ISOLATED\"],\n [\"daemon\", \"ISOLATED\"],\n [\"daemon.log\", \"ISOLATED\"],\n // SHARED — directories only (see policy doc above)\n [\"projects\", \"SHARED\"],\n [\"sessions\", \"SHARED\"],\n [\"tasks\", \"SHARED\"],\n [\"todos\", \"SHARED\"],\n [\"transcripts\", \"SHARED\"],\n [\"shell-snapshots\", \"SHARED\"],\n // The underscored variant is the historical exclude-list name; some\n // Claude Code versions may still use it. Classify SHARED so either\n // spelling resolves correctly.\n [\"shell_snapshots\", \"SHARED\"],\n [\"plans\", \"SHARED\"],\n [\"file-history\", \"SHARED\"],\n [\"backups\", \"SHARED\"],\n])\n\nfunction policyFor(name: string): MirrorPolicy {\n return CLAUDE_HOME_POLICY.get(name) ?? \"MIRRORED\"\n}\n\n/**\n * Test-only export: lets the test suite assert hard regression guards\n * such as `policyFor(\"agents\") === \"MIRRORED\"` (preventing accidental\n * reclassification that would let `sweepStalePeerAgentMdFiles` delete\n * files in the user's real `~/.claude/agents/`).\n */\nexport const __testing = { policyFor, ensureSharedSymlink }\n\n/**\n * Names with `SHARED` policy, materialized once for iteration in\n * `ensureClaudeConfigMirror`'s post-copy phase.\n */\nconst SHARED_TOPLEVEL_NAMES: ReadonlyArray<string> = Array.from(\n CLAUDE_HOME_POLICY.entries(),\n)\n .filter(([, kind]) => kind === \"SHARED\")\n .map(([name]) => name)\n\n/**\n * Marker file written into the router-owned CLAUDE_CONFIG_DIR so users\n * (and our own future sweeps) can identify that the dir is managed by\n * github-router. Content is informational only; no logic depends on\n * its presence.\n */\nconst MANAGED_MARKER_FILENAME = \".github-router-managed\"\n\n/**\n * Synthetic Console OAuth credential the router writes into its own\n * `CLAUDE_CONFIG_DIR/.credentials.json` so spawned Claude Code (and\n * any teammates it spawns) can authenticate without a real user\n * `/login`.\n *\n * Schema verified verbatim from `claude` v2.1.140 binary, function\n * `guH` (the credentials-save mutation). Fields:\n * - `accessToken` — sent as `Authorization: Bearer ...` to the\n * proxy. Proxy accepts any bearer (per CLAUDE.md \"doesn't enforce\n * auth\").\n * - `refreshToken` — only used by Claude Code's reactive refresh\n * path (function `nH8`), which fires on 401 from upstream. The\n * proxy maintains the no-401 invariant on the Anthropic-shape\n * boundary, so this is never invoked. Synthetic value is fine.\n * - `expiresAt` — far-future (2099-01-01 ms epoch). Sidesteps the\n * proactive refresh path (`R8H(expiresAt)` returns false).\n * - `scopes` — claude-ai-shaped so `tB(scopes)` returns true,\n * making `Hq()` true (full feature surface, not \"inference only\").\n * - `subscriptionType` — `\"max\"`. Pure client-side label\n * (`e7()` / `Zc_()` / `CZ1()`); no server validation since\n * `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` suppresses\n * subscription-validation calls. Picks the most-permissive gating.\n * - `rateLimitTier` — `\"default_claude_max_20x\"`. Paired with\n * `subscriptionType:\"max\"` this is the real Max-20x tier, so the\n * credential is internally consistent (vs the prior odd `max`+`null`).\n * Verified live (claude v2.1.158) call-sites are cosmetic billing /\n * upsell-suppression UI plus the `getPlanModeV2AgentCount` (`bGK`)\n * `max && 20x → 3` branch — which `CLAUDE_CODE_PLAN_V2_AGENT_COUNT`\n * (set to 7 in server-setup) already overrides, so this is\n * belt-and-suspenders for the natural code path. No client-side quota\n * enforcement keys off the tier (rate-limit UI reads server\n * `x-ratelimit-*` headers; the proxy holds the no-429 invariant).\n */\nconst SYNTHETIC_CREDENTIAL = {\n claudeAiOauth: {\n accessToken: \"github-router-synthetic\",\n refreshToken: \"github-router-synthetic\",\n expiresAt: 4_070_908_800_000,\n scopes: [\"user:inference\", \"user:profile\"],\n subscriptionType: \"max\",\n rateLimitTier: \"default_claude_max_20x\",\n clientId: \"github-router\",\n },\n} as const\n\n/**\n * Router-owned fields injected into `<CLAUDE_CONFIG_DIR>/.claude.json` so\n * Claude Code skips its first-launch onboarding wizard. The synthetic\n * `.credentials.json` covers the OAuth *token*, but on a fresh machine\n * the wizard runs anyway — and one of its steps is the browser-OAuth\n * \"Sign in to your Anthropic account\" prompt that the user sees today.\n *\n * Schema verified against `claude` v2.1.158 binary strings:\n *\n * - `hasCompletedOnboarding: true` — single load-bearing gate.\n * Binary code path: `if (!E_().hasCompletedOnboarding) { ... await\n * Onboarding.default(...) }`. Setting true causes the wizard\n * (including the OAuth login step) to be skipped entirely.\n *\n * - `bypassPermissionsModeAccepted: true` — skips the\n * `--dangerously-skip-permissions` trust disclaimer. Binary gate:\n * `K = Ip() || Boolean(E_().bypassPermissionsModeAccepted)`. The\n * proxy passes `--dangerously-skip-permissions` so `Ip()` typically\n * suffices, but pinning the field covers edge code paths (e.g.\n * `--bg --dangerously-skip-permissions`) that still consult it\n * directly and would otherwise throw \"requires accepting the\n * disclaimer first.\"\n *\n * - `oauthAccount` — nice-to-have for UI consistency. Claude Code\n * reads sub-fields with optional chaining (`oauthAccount?.accountUuid`,\n * `oauthAccount?.organizationUuid`, `oauthAccount?.organizationRole`)\n * so undefined doesn't trigger login — but populating gives any\n * \"logged in as X\" / status-line UI something deterministic and\n * obviously-synthetic to display. Values are intentionally\n * non-credible (`github-router@local`, all-zero UUIDs) so any\n * leak into logs is self-documenting, not impersonation.\n *\n * Merge semantics: the two boolean gates are **force-overridden** —\n * even an existing `false` is flipped to `true`. (A user logging out\n * via the Claude Code UI cannot defeat the proxy-session bypass.)\n * `oauthAccount` is overwritten only when the existing value isn't a\n * *structurally usable* one (presence-only would preserve `{}` or\n * `{foo:1}` shapes that defeat the fix); a real OAuth identity with\n * non-empty `accountUuid` and `organizationUuid` is preserved as-is.\n * Every OTHER top-level field the user brought in via the mirror walk\n * (settings, project history, user-side MCP entries, etc.) is preserved\n * verbatim. The mirror is a router-controlled view, not a faithful\n * copy — same trade-off the synthetic credential already makes.\n */\nconst SYNTHETIC_CLAUDE_JSON_FIELDS = {\n hasCompletedOnboarding: true,\n bypassPermissionsModeAccepted: true,\n oauthAccount: {\n accountUuid: \"00000000-0000-0000-0000-000000000000\",\n organizationUuid: \"00000000-0000-0000-0000-000000000000\",\n organizationRole: \"member\",\n emailAddress: \"github-router@local\",\n organizationName: \"github-router\",\n },\n} as const\n\n/**\n * Snapshot-copy the user's `~/.claude/` into the router-owned\n * CLAUDE_CONFIG_DIR (real files, not symlinks — symlinks don't isolate\n * writes), classifying each top-level entry per `CLAUDE_HOME_POLICY`:\n * ISOLATED entries are skipped, MIRRORED entries are copied, and\n * SHARED entries become directory symlinks back to `~/.claude/<X>` so\n * chat history (in `projects/<cwd-hash>/<session-uuid>.jsonl`) and\n * other durable user state flow between proxy and plain-`claude`\n * sessions. Then writes the synthetic `.credentials.json` so spawned\n * Claude Code (and teammates that inherit `CLAUDE_CONFIG_DIR`)\n * authenticate.\n *\n * Idempotent: only re-copies files whose source `mtime` is newer than\n * target; SHARED-symlink creation no-ops when the symlink already\n * points at the right target. Concurrent-safe: `mkdir({recursive:true})`\n * is idempotent; symlinks are created via atomic temp+rename so two\n * parallel github-router-claude startups can't race to EEXIST; the\n * credentials write uses temp-file + atomic rename so Claude Code's\n * `EZ1()` mtime watcher never sees a partial write.\n *\n * Walks with `lstat` (does NOT follow symlinks during traversal — a\n * symlink-into-`/` would otherwise let the walk escape). Symlink leaves\n * in the source tree are skipped during the MIRRORED copy walk (per the\n * symlink-confused-deputy security finding); SHARED symlinks are\n * created on the mirror side only, pointing at predetermined targets\n * inside the user's real `~/.claude/`.\n *\n * Caller is expected to invoke this after `ensurePaths()` and before\n * spawning Claude Code (`launchChild`). The mirror must exist before\n * the child reads it. Currently called from the `claude` subcommand\n * entry point only; `start` and `codex` subcommands don't need it.\n */\nexport async function ensureClaudeConfigMirror(opts: {\n realHome?: string\n} = {}): Promise<void> {\n const realHome = opts.realHome ?? os.homedir()\n const sourceDir = path.join(realHome, \".claude\")\n const targetDir = PATHS.CLAUDE_CONFIG_DIR\n\n // 1. Create our config dir (idempotent, mode 0o700)\n await fs.mkdir(targetDir, { recursive: true, mode: 0o700 })\n await chmodIfPossible(targetDir, 0o700)\n\n // 2. Snapshot-copy from ~/.claude if it exists. Only MIRRORED entries\n // flow through this walk; ISOLATED and SHARED entries are filtered\n // in `mirrorDirRecursive` and handled separately.\n let sourceExists = false\n try {\n const sourceStat = await fs.stat(sourceDir)\n sourceExists = sourceStat.isDirectory()\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot stat ${sourceDir}:`, err)\n }\n }\n if (sourceExists) {\n await mirrorDirRecursive(sourceDir, targetDir, \"\")\n }\n\n // 3. Always ensure agents/ exists (even if user has none) so the\n // peer-agent .md emission has a place to write. Empty dir is fine.\n // agents/ is MIRRORED, not SHARED — the proxy writes per-launch\n // `peer-<pid>-*.md` files here and `sweepStalePeerAgentMdFiles`\n // deletes them; routing those operations into the user's real\n // `~/.claude/agents/` would destroy their custom subagent files.\n await fs.mkdir(path.join(targetDir, \"agents\"), { recursive: true })\n\n // 4. Create symlinks for SHARED entries so chat history (and other\n // durable user state) is visible in both proxy and plain-`claude`.\n for (const name of SHARED_TOPLEVEL_NAMES) {\n await ensureSharedSymlink(name, sourceDir, targetDir).catch((err) => {\n consola.debug(\n `ensureClaudeConfigMirror: SHARED symlink for ${name} skipped:`,\n err,\n )\n })\n }\n\n // 5. Write synthetic .credentials.json (only if content differs)\n const credentialsPath = path.join(targetDir, \".credentials.json\")\n const desiredJson = JSON.stringify(SYNTHETIC_CREDENTIAL, null, 2)\n let needsWrite = true\n try {\n const existing = await fs.readFile(credentialsPath, \"utf8\")\n needsWrite = existing.trim() !== desiredJson.trim()\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot read existing credentials:`, err)\n }\n }\n if (needsWrite) {\n // Atomic temp-file + rename so EZ1()'s mtime watcher doesn't see\n // a partial write. wx flag ensures we don't clobber a concurrent\n // writer's tempfile.\n const tempPath = `${credentialsPath}.${process.pid}.tmp`\n try {\n await fs.writeFile(tempPath, desiredJson + \"\\n\", { mode: 0o600, flag: \"wx\" })\n await fs.rename(tempPath, credentialsPath)\n } catch (err) {\n // EEXIST on the tempfile means another concurrent startup is\n // mid-write. Best-effort: skip — the other writer will produce\n // identical content (deterministic constant blob).\n if ((err as NodeJS.ErrnoException).code === \"EEXIST\") {\n consola.debug(\n \"ensureClaudeConfigMirror: concurrent credentials-write detected, skipping\",\n )\n } else {\n await fs.unlink(tempPath).catch(() => {})\n throw err\n }\n }\n }\n await chmodIfPossible(credentialsPath, 0o600)\n\n // 6. Inject onboarding-skip fields into <CLAUDE_CONFIG_DIR>/.claude.json.\n // Runs AFTER the mirror walk (step 2) so a user's existing\n // .claude.json content (settings, projects, user-side mcpServers)\n // is preserved, and BEFORE `injectPeerMcpIntoMirror` (called later\n // from `src/claude.ts`) which read-merge-writes the same file and\n // will preserve our fields naturally.\n //\n // Without this step, on a brand-new machine where ~/.claude/.claude.json\n // doesn't exist, nothing lands in the mirror, Claude Code's\n // `hasCompletedOnboarding` gate defaults to falsy, and the\n // first-launch wizard runs — including the \"Sign in to your\n // Anthropic account\" OAuth step that defeats the synthetic\n // credential. See SYNTHETIC_CLAUDE_JSON_FIELDS for the gate\n // rationale + binary-verified field shape.\n await injectSyntheticClaudeJsonFields(targetDir, sourceDir, realHome)\n\n // 6a. Postcondition: verify the gate fields actually landed in the\n // mirror's .claude.json. The helper has several \"warn and\n // return\" branches (non-regular target, non-ENOENT read failure)\n // where falling through silently would let Claude Code's\n // first-launch wizard fire — the exact bug this whole\n // subsystem exists to prevent. Re-reading the file and\n // asserting the booleans converts every silent-fail path into a\n // fail-loud launch error, which `src/claude.ts` surfaces to the\n // user with `process.exit(1)`. Cheap (one extra readFile +\n // JSON.parse on a small file we just wrote) and gives the\n // no-silent-degradation invariant teeth even if a future\n // contributor adds a new bail-out branch.\n await assertOnboardingGateInjected(targetDir)\n\n // 7. Write/refresh marker file. Use lstat (not access) to detect\n // symlinks at the marker path — a previously-mirrored or\n // user-placed symlink could otherwise let our `fs.writeFile`\n // follow through to an arbitrary target. With the symlink-skip\n // policy in `mirrorDirRecursive` this is defense-in-depth, but\n // cheap and definitive.\n const markerPath = path.join(targetDir, MANAGED_MARKER_FILENAME)\n let markerExists = false\n try {\n const markerStat = await fs.lstat(markerPath)\n if (markerStat.isFile()) {\n markerExists = true\n } else {\n // Anything non-regular (symlink, dir, special file) is a red flag —\n // refuse to overwrite, log loudly. The user can investigate.\n consola.warn(\n `ensureClaudeConfigMirror: ${markerPath} exists but is not a regular file (mode=${markerStat.mode.toString(8)}); refusing to overwrite. Inspect and remove manually if safe.`,\n )\n markerExists = true\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`ensureClaudeConfigMirror: cannot lstat marker:`, err)\n markerExists = true\n }\n }\n if (!markerExists) {\n const body = `Managed by github-router. Created ${new Date().toISOString()}. Safe to delete (will be recreated).\\n`\n // wx flag (O_CREAT | O_EXCL) refuses to clobber an existing\n // file or symlink (POSIX O_EXCL behavior) — additional protection\n // against the marker-symlink confused-deputy vector.\n await fs\n .writeFile(markerPath, body, { mode: 0o600, flag: \"wx\" })\n .catch((err) => {\n consola.debug(`ensureClaudeConfigMirror: marker write skipped:`, err)\n })\n }\n}\n\n/**\n * Read-merge-atomic-write the router-required onboarding-skip fields\n * (see `SYNTHETIC_CLAUDE_JSON_FIELDS`) into `<targetDir>/.claude.json`.\n *\n * Inputs:\n * - `targetDir` — the per-launch mirror dir we own.\n * - `sourceDir` — the user's `~/.claude/` (subdir-level legacy path).\n * - `realHome` — the user's `$HOME`. Used to locate the **canonical**\n * `~/.claude.json` (HOME level), which is what Claude Code's path\n * resolver `qG` actually reads when `CLAUDE_CONFIG_DIR` is unset:\n * `path.join(process.env.CLAUDE_CONFIG_DIR || homedir(),\n * \".claude.json\")`. The mirror walk operates on `~/.claude/` and\n * thus can only see the subdir-level `~/.claude/.claude.json`\n * (often a stale shadow); the canonical HOME-level file — where\n * `claude mcp add` writes and Claude Code reads on default\n * invocations — would otherwise be invisible to the proxy session.\n *\n * Behaviour:\n * - Mirror's `.claude.json` is a non-regular file (symlink, dir,\n * etc.) → log warn, refuse to touch. The postcondition check in\n * `ensureClaudeConfigMirror` then throws, blocking launch — the\n * user investigates the warn-flagged path rather than landing in\n * the OAuth wizard.\n * - Read existing content from the first VALID source, in priority\n * order:\n * (a) `~/.claude.json` HOME-level (canonical Claude Code path)\n * (b) The mirror file itself (snapshot-walk's product; only\n * consulted if it's a regular file per step 1)\n * (c) `~/.claude/.claude.json` subdir-level (legacy)\n * A candidate that's missing (ENOENT), too large (> 50 MiB cap),\n * or fails to parse as a non-empty object is **skipped** — the\n * loop falls through to the next candidate, so a malformed\n * higher-priority source can't drop valid lower-priority content\n * on the floor. HOME-level non-ENOENT read failures warn\n * (user-visible — corp perms / OneDrive). All other failures\n * debug-log. `fs.readFile` follows symlinks; reading user-owned\n * content into a per-launch user-owned dir is not a privilege\n * escalation (same uid).\n * - Merge: **force-override** all three synthetic fields\n * unconditionally — `hasCompletedOnboarding`,\n * `bypassPermissionsModeAccepted`, AND `oauthAccount`. A\n * within-proxy \"log out\" is impossible by design. `oauthAccount`\n * is overwritten even when the user has a real one, because\n * pairing the synthetic OAuth token (from `.credentials.json`)\n * with a real-user identity blob creates a split-brain auth\n * state that Claude Code's identity-vs-token cross-checks may\n * treat as session corruption (triggering re-login → defeats\n * the fix). The user's real identity remains intact in their\n * own `~/.claude.json`; only the proxy session sees synthetic.\n * `Object.assign(Object.create(null), existing)` instead of spread\n * defuses the `__proto__` prototype-pollution sink from JSON.parse.\n * - Idempotency: skip the write IFF the source we read FROM is the\n * mirror itself AND the current mirror content is byte-equivalent\n * to what we'd write. Bytes-on-disk comparison is robust against\n * filesystem mtime resolution (FAT/exFAT 2 s buckets) and the\n * source-presence-vs-mirror-presence conflation that broke\n * round-2's `needsWrite = !hadExisting` heuristic.\n *\n * Atomic write: tempfile (per-PID + 4-byte random suffix matching\n * `injectPeerMcpIntoMirror`'s pattern, so collision is effectively\n * impossible) + rename, mode 0o600. Pre-chmod the destination before\n * rename to defuse Windows read-only attribute issues. Any write\n * error throws to the caller — `src/claude.ts` fails the launch\n * loudly. Combined with the postcondition check, every silent-\n * degradation path is converted to a fail-loud launch error.\n */\nasync function injectSyntheticClaudeJsonFields(\n targetDir: string,\n sourceDir: string,\n realHome: string,\n): Promise<void> {\n const claudeJsonPath = path.join(targetDir, \".claude.json\")\n\n // 1. lstat the mirror path before touching it (defense-in-depth,\n // matching the marker-write at step 7). If something other than a\n // regular file occupies the slot, refuse — `fs.readFile` /\n // `chmodIfPossible` / `fs.rename` would otherwise follow / clobber\n // in surprising ways. The postcondition then throws.\n let mirrorIsRegularFile = false\n try {\n const mirrorStat = await fs.lstat(claudeJsonPath)\n if (mirrorStat.isFile()) {\n mirrorIsRegularFile = true\n } else {\n consola.warn(\n `ensureClaudeConfigMirror: ${claudeJsonPath} exists but is not a regular file (mode=${mirrorStat.mode.toString(8)}); refusing to inject synthetic fields. Inspect and remove manually if safe.`,\n )\n return\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.warn(\n `ensureClaudeConfigMirror: cannot lstat ${claudeJsonPath}; refusing to inject synthetic fields (overwriting an unreadable file would risk destroying user content):`,\n err,\n )\n return\n }\n // ENOENT: mirror file doesn't exist yet — fine; step 2 skips the\n // mirror candidate and step 4 creates a new file.\n }\n\n // 2. Read existing content from the first VALID source. Priority:\n // (a) `~/.claude.json` HOME-level (canonical Claude Code path)\n // (b) Mirror file (the snapshot-walk's product; only if regular)\n // (c) `~/.claude/.claude.json` subdir-level (legacy)\n // A candidate that's missing (ENOENT) or fails to parse as a\n // non-empty object is SKIPPED — the loop falls through to the\n // next candidate. This is the load-bearing fix for \"corrupt HOME\n // blocks valid subdir\": a zero-byte or malformed HOME file no\n // longer drops the user's good subdir content on the floor.\n // Size cap (`MAX_CLAUDE_JSON_BYTES`) skips absurdly large files\n // that would risk OOM. HOME read failures (non-ENOENT) warn\n // loudly — the canonical path failing is user-visible. Mirror /\n // subdir read failures debug-log and fall through.\n // `fs.readFile` follows symlinks; reading user-owned content\n // into a per-launch user-owned dir is not a privilege escalation.\n let existing: Record<string, unknown> = {}\n let selectedSource: string | null = null\n const homeLevelClaudeJson = path.join(realHome, \".claude.json\")\n const subdirClaudeJson = path.join(sourceDir, \".claude.json\")\n const sourceCandidates: Array<string | null> = [\n homeLevelClaudeJson,\n mirrorIsRegularFile ? claudeJsonPath : null,\n subdirClaudeJson,\n ]\n for (const sourceCandidate of sourceCandidates) {\n if (sourceCandidate === null) continue\n try {\n const stat = await fs.stat(sourceCandidate)\n if (stat.size > MAX_CLAUDE_JSON_BYTES) {\n consola.warn(\n `ensureClaudeConfigMirror: ${sourceCandidate} is ${stat.size} bytes (> ${MAX_CLAUDE_JSON_BYTES} cap); skipping this source to avoid OOM. Inspect for accidental log/state accumulation.`,\n )\n continue\n }\n const raw = await fs.readFile(sourceCandidate, \"utf8\")\n const parsed = parseExistingClaudeJson(raw, sourceCandidate)\n if (parsed === null) {\n // Malformed (parse failure or non-object). Skip; try next candidate.\n continue\n }\n existing = parsed\n selectedSource = sourceCandidate\n consola.debug(\n `ensureClaudeConfigMirror: read .claude.json content from ${sourceCandidate}`,\n )\n break\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === \"ENOENT\") {\n // Normal case — skip silently.\n continue\n }\n // Non-ENOENT read/stat failure. Severity depends on which source:\n // - HOME-level (canonical): warn (user-visible — corp perms,\n // OneDrive cloud-only, EACCES, etc.). Fall through, since\n // the gate-injection still needs to happen.\n // - Mirror / subdir: debug. Mirror failures used to abort,\n // but that locked out the still-valid subdir-level fallback;\n // we can always re-create the mirror from any valid source.\n if (sourceCandidate === homeLevelClaudeJson) {\n consola.warn(\n `ensureClaudeConfigMirror: cannot read canonical ${homeLevelClaudeJson}; falling back to other sources. User-scope MCPs added via 'claude mcp add' may be invisible to the proxy session:`,\n err,\n )\n } else {\n consola.debug(\n `ensureClaudeConfigMirror: cannot read source ${sourceCandidate}:`,\n err,\n )\n }\n // Fall through to next candidate. If all miss/fail, proceed\n // with synthetic-only content (existing stays {}).\n }\n }\n\n // 3. Build merged content. `Object.assign(Object.create(null), ...)`\n // instead of `{...existing}` avoids the `__proto__` prototype-\n // pollution sink (`JSON.parse('{\"__proto__\":{...}}')` sets the\n // object's prototype via defineProperty; spreading then triggers\n // the standard `__proto__` setter and mutates merged's prototype\n // chain. Cheap defense, prevents the entire class.\n //\n // All three gate fields are FORCE-OVERRIDDEN regardless of\n // existing value:\n // - `hasCompletedOnboarding` and `bypassPermissionsModeAccepted`\n // are the load-bearing gates we exist to set; a within-proxy\n // \"log out\" is impossible by design.\n // - `oauthAccount` is overwritten unconditionally with the\n // synthetic identity (NOT preserved even when real). Pairing\n // the synthetic OAuth token (from `.credentials.json`) with\n // a real-user `accountUuid` / `organizationUuid` creates a\n // split-brain auth state where Claude Code's identity-vs-\n // token cross-checks (binary `oauthAccount?.accountUuid`\n // consumers) may treat the mismatch as session corruption\n // and trigger re-login — the exact bug we're preventing.\n // The user's real identity remains intact in their own\n // `~/.claude.json`; only the proxy session sees synthetic.\n const merged: Record<string, unknown> = Object.assign(\n Object.create(null) as Record<string, unknown>,\n existing,\n )\n merged.hasCompletedOnboarding = SYNTHETIC_CLAUDE_JSON_FIELDS.hasCompletedOnboarding\n merged.bypassPermissionsModeAccepted = SYNTHETIC_CLAUDE_JSON_FIELDS.bypassPermissionsModeAccepted\n merged.oauthAccount = SYNTHETIC_CLAUDE_JSON_FIELDS.oauthAccount\n\n // 4. Decide if a write is needed. Idempotency contract: skip the\n // write IFF we read FROM the mirror itself AND the current mirror\n // content is byte-equivalent to what we'd write. Bytes-on-disk\n // comparison (vs. inferring from source state) is robust against:\n // - source-presence ≠ mirror-presence (a fully-onboarded HOME\n // file with no mirror yet must still trigger a mirror write)\n // - HOME content newer than mirror (HOME wins → mirror is\n // always rewritten when source was HOME)\n // - filesystem mtime resolution (FAT/exFAT 2s buckets)\n const desiredJson = JSON.stringify(merged, null, 2) + \"\\n\"\n if (selectedSource === claudeJsonPath) {\n try {\n const currentMirrorJson = await fs.readFile(claudeJsonPath, \"utf8\")\n if (currentMirrorJson === desiredJson) {\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n return\n }\n } catch {\n // Mirror read failed between step 2's success and now — fall\n // through to write. Worst case we rewrite identical content.\n }\n }\n\n // 5. Atomic temp + rename. Mode 0o600. Per-PID + 4-byte random\n // suffix matches `injectPeerMcpIntoMirror`'s pattern. Any write\n // error throws to the caller — `src/claude.ts` exits the launch\n // with a loud error message rather than silently degrading to\n // the OAuth wizard.\n //\n // Pre-chmod the destination before rename to defuse Windows\n // read-only attribute issues (FAT/exFAT volumes, corp-managed\n // perms): `fs.rename` over a read-only file can fail with EPERM\n // on Windows even though POSIX would silently succeed.\n //\n // Concurrent-racer tolerance: when multiple `ensureClaudeConfigMirror()`\n // calls run in parallel (e.g., from tests, or a future caller),\n // Windows `fs.rename` can fail with EBUSY/EPERM/EACCES if\n // another racer has the destination open or mid-write. The\n // write content is fully deterministic (same SYNTHETIC fields,\n // same force-override merge of the same source-priority list),\n // so a successful rename by ANY racer produces the same bytes\n // we'd produce. On rename failure, verify the on-disk content\n // matches `desiredJson` — if so, accept silently (another\n // racer won the race; the file is correct).\n const tempPath = `${claudeJsonPath}.${process.pid}.${randomBytes(4).toString(\"hex\")}.tmp`\n try {\n await fs.writeFile(tempPath, desiredJson, { mode: 0o600, flag: \"wx\" })\n if (mirrorIsRegularFile) {\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n }\n await fs.rename(tempPath, claudeJsonPath)\n } catch (err) {\n await fs.unlink(tempPath).catch(() => {})\n try {\n const observed = await fs.readFile(claudeJsonPath, \"utf8\")\n if (observed === desiredJson) {\n consola.debug(\n `ensureClaudeConfigMirror: rename failed but mirror already holds expected content (concurrent racer won the race):`,\n err,\n )\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n return\n }\n } catch {\n // Fall through to rethrow the original error.\n }\n throw err\n }\n await chmodIfPossible(claudeJsonPath, 0o600).catch(() => {})\n}\n\n/**\n * Size cap on candidate `.claude.json` reads in\n * `injectSyntheticClaudeJsonFields`. Real-world files are KB-MB scale;\n * if a `.claude.json` has grown to hundreds of MB (corruption, runaway\n * project history accumulation, accidental log redirect) reading it\n * blocks the event loop and risks OOM. 50 MiB is comfortably above\n * any legitimate Claude Code state and well below typical RSS budgets.\n */\nconst MAX_CLAUDE_JSON_BYTES = 50 * 1024 * 1024\n\n/**\n * Structural check for `oauthAccount`: must be a non-null, non-array\n * object whose `accountUuid` AND `organizationUuid` are non-empty\n * strings. Empty objects / partial blobs (missing UUIDs) do NOT count\n * as \"usable\" — preserving them would let Claude Code's various\n * \"logged in as X\" UIs read undefined sub-fields and (in some flows)\n * fall back to a re-login prompt that defeats the synthetic-credential\n * fix. Field names match the binary's optional-chain reads\n * (`oauthAccount?.accountUuid`, `oauthAccount?.organizationUuid`).\n */\nfunction isUsableOauthAccount(value: unknown): boolean {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false\n const obj = value as Record<string, unknown>\n const accountUuid = obj.accountUuid\n const organizationUuid = obj.organizationUuid\n return (\n typeof accountUuid === \"string\"\n && accountUuid.length > 0\n && typeof organizationUuid === \"string\"\n && organizationUuid.length > 0\n )\n}\n\n/**\n * Parse a JSON blob expected to be an object. Returns `null` on parse\n * failure or non-object/array value (so callers can SKIP this source\n * rather than treating an empty object as a successful read — a\n * round-1 finding from gemini-critic). Logs a warn so the user can\n * investigate. Used in `injectSyntheticClaudeJsonFields`'s source-\n * priority loop: a malformed HOME-level file falls through to the\n * mirror or subdir candidate instead of dropping valid lower-priority\n * content on the floor.\n */\nfunction parseExistingClaudeJson(\n raw: string,\n contextPath: string,\n): Record<string, unknown> | null {\n try {\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n consola.warn(\n `ensureClaudeConfigMirror: ${contextPath} parsed to non-object `\n + `(typeof=${typeof parsed}); skipping this source.`,\n )\n return null\n } catch (err) {\n consola.warn(\n `ensureClaudeConfigMirror: cannot parse ${contextPath} as JSON; `\n + `skipping this source:`,\n err,\n )\n return null\n }\n}\n\n/**\n * Postcondition for `ensureClaudeConfigMirror`: re-reads the mirror's\n * `.claude.json` and verifies that both boolean gate fields actually\n * landed. Converts every \"warn and return\" branch in\n * `injectSyntheticClaudeJsonFields` (non-regular target, non-ENOENT\n * read failure, postcondition lstat race) into a launch-failing\n * throw — `src/claude.ts:197`'s catch path turns it into a clear\n * `process.exit(1)` rather than silently spawning Claude Code into\n * the OAuth wizard. The whole point of this subsystem is preventing\n * that silent degradation; a warn that scrolls by in the launch log\n * is not enough.\n *\n * Cheap (one extra readFile + JSON.parse on a small file we just\n * wrote) and load-bearing: every future contributor who adds a new\n * bail-out branch in the helper above automatically gets fail-loud\n * for free instead of having to remember to thread the failure\n * upward by hand.\n */\nasync function assertOnboardingGateInjected(targetDir: string): Promise<void> {\n const claudeJsonPath = path.join(targetDir, \".claude.json\")\n\n // 1. lstat first — never follow a symlink. A planted symlink at the\n // mirror path pointing at an attacker-controlled file with valid\n // gate fields would otherwise pass a plain readFile check and let\n // the helper's \"non-regular target\" warn-and-return slip through.\n let lst: import(\"node:fs\").Stats\n try {\n lst = await fs.lstat(claudeJsonPath)\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — cannot lstat ${claudeJsonPath} after injection (synthetic onboarding-skip fields are required to prevent Claude Code's first-launch wizard, which would defeat the synthetic credential): ${(err as Error).message}`,\n )\n }\n if (!lst.isFile()) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} exists but is not a regular file (mode=${lst.mode.toString(8)}). Check the preceding warn-level log lines for the underlying cause; without a regular file holding the gate fields, Claude Code's first-launch wizard fires and defeats the synthetic credential.`,\n )\n }\n\n let raw: string\n try {\n raw = await fs.readFile(claudeJsonPath, \"utf8\")\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — cannot read ${claudeJsonPath} after injection: ${(err as Error).message}`,\n )\n }\n let parsed: Record<string, unknown>\n try {\n const json = JSON.parse(raw) as unknown\n if (!json || typeof json !== \"object\" || Array.isArray(json)) {\n throw new Error(`parsed to non-object (typeof=${typeof json})`)\n }\n parsed = json as Record<string, unknown>\n } catch (err) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} is not a valid JSON object after injection: ${(err as Error).message}`,\n )\n }\n if (\n parsed.hasCompletedOnboarding !== true\n || parsed.bypassPermissionsModeAccepted !== true\n ) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} is missing required gate fields after injection (hasCompletedOnboarding=${String(parsed.hasCompletedOnboarding)}, bypassPermissionsModeAccepted=${String(parsed.bypassPermissionsModeAccepted)}). Check the preceding warn-level log lines for the underlying cause; without these fields Claude Code's first-launch wizard fires and defeats the synthetic credential.`,\n )\n }\n // Reuse `isUsableOauthAccount` so a future change to the structural\n // definition stays consistent across helper write-decision and\n // postcondition. The synthetic blob we always write satisfies this\n // by construction; a regression that drops oauthAccount during the\n // merge would otherwise slip past (per the round-2 codex/opus/\n // gemini-3-lab finding).\n if (!isUsableOauthAccount(parsed.oauthAccount)) {\n throw new Error(\n `ensureClaudeConfigMirror: postcondition failed — ${claudeJsonPath} has invalid oauthAccount after injection (got ${JSON.stringify(parsed.oauthAccount)}). The synthetic identity blob is required so Claude Code's identity-vs-token cross-checks don't trigger a re-login that defeats the synthetic credential.`,\n )\n }\n}\n\n/**\n * Recursive snapshot-copy helper for `ensureClaudeConfigMirror`. Walks\n * `sourceDir/relPath` and mirrors each entry into `targetDir/relPath`.\n * - Top-level entries are dispatched on `policyFor(name)`:\n * - `ISOLATED` → skipped entirely (no presence in mirror).\n * - `SHARED` → skipped from the copy walk; handled by\n * `ensureSharedSymlink` in the post-copy phase.\n * - `MIRRORED` → copied as today.\n * - Symlinks are skipped (not recreated) so the walk never follows out\n * of `sourceDir` and we don't reintroduce a confused-deputy vector.\n * - Files copy only if source mtime > target mtime (idempotent).\n */\nasync function mirrorDirRecursive(\n sourceDir: string,\n targetDir: string,\n relPath: string,\n): Promise<void> {\n const sourcePath = path.join(sourceDir, relPath)\n let entries: Array<string>\n try {\n entries = await fs.readdir(sourcePath)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n consola.debug(`mirrorDirRecursive: cannot readdir ${sourcePath}:`, err)\n return\n }\n for (const name of entries) {\n // Policy dispatch at top-level only. Sub-paths within MIRRORED\n // dirs always cascade as MIRRORED.\n if (relPath === \"\") {\n const policy = policyFor(name)\n if (policy === \"ISOLATED\" || policy === \"SHARED\") continue\n }\n const childRel = relPath === \"\" ? name : path.join(relPath, name)\n const childSource = path.join(sourceDir, childRel)\n const childTarget = path.join(targetDir, childRel)\n let stats: Awaited<ReturnType<typeof fs.lstat>>\n try {\n stats = await fs.lstat(childSource)\n } catch (err) {\n consola.debug(`mirrorDirRecursive: cannot lstat ${childSource}:`, err)\n continue\n }\n if (stats.isSymbolicLink()) {\n // Skip symlinks during mirror copy. gemini-critic security finding:\n // recreating user symlinks in our mirror creates a confused-deputy\n // vector — a previously prompt-injected process could place\n // `~/.claude/<X>` → `/some/sensitive/file`, our walker would mirror\n // it, and any subsequent write to `<mirror>/<X>` (by us or by\n // Claude Code) would follow the symlink and overwrite the target.\n // Snapshot-copy semantics make symlink preservation moot anyway:\n // a snapshot is a point-in-time content copy, and a symlink\n // recreated in the mirror points at exactly the same target as\n // the original would have — the user-side symlink is sufficient.\n // If a user has a legitimate need for a symlink to be visible\n // through the proxy session, they can create the equivalent\n // symlink in their `~/.claude/` directly and it'll be reachable\n // — they just won't see it in our mirror dir.\n consola.debug(`mirrorDirRecursive: skipping symlink ${childSource} (security policy)`)\n continue\n }\n if (stats.isDirectory()) {\n await fs.mkdir(childTarget, { recursive: true })\n await mirrorDirRecursive(sourceDir, targetDir, childRel)\n continue\n }\n if (stats.isFile()) {\n // mtime-based skip — only copy if source is newer than target.\n let needsCopy = true\n try {\n const targetStat = await fs.lstat(childTarget)\n if (targetStat.isFile() && targetStat.mtimeMs >= stats.mtimeMs) {\n needsCopy = false\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n consola.debug(`mirrorDirRecursive: lstat target ${childTarget}:`, err)\n }\n }\n if (!needsCopy) continue\n try {\n await fs.copyFile(childSource, childTarget, fs.constants.COPYFILE_FICLONE)\n } catch (err) {\n consola.debug(`mirrorDirRecursive: copy ${childSource} → ${childTarget}:`, err)\n }\n continue\n }\n // Skip other inode types (sockets, devices, fifos) silently.\n }\n}\n\n/**\n * Create or refresh a directory symlink `<mirrorDir>/<name>` →\n * `<sourceDir>/<name>` (i.e. `~/.local/share/github-router/claude-config/<X>`\n * → `~/.claude/<X>`). Idempotent and concurrent-safe.\n *\n * Behavior depending on what's already at `<mirrorDir>/<name>`:\n * - Symlink with the correct target → no-op.\n * - Symlink with the wrong target → replace atomically.\n * - Empty real directory (legacy mirror leftover with no proxy-session\n * writes accumulated yet) → `rmdir` and replace with the symlink.\n * Safe by definition: `fs.rmdir` only succeeds on empty dirs (POSIX),\n * so there is nothing to lose. Smooths the upgrade path for users\n * whose legacy mirror dirs were never written to.\n * - Non-empty real directory or regular file → loud-warn and skip.\n * Auto-deleting would destroy proxy-session writes from the prior\n * version. The user is told the exact path and remediation.\n * - ENOENT → create symlink atomically.\n *\n * Atomic-creation: symlinks are first written at a unique side-path\n * (`<mirrorDir>/<name>.tmp.<pid>.<8 hex>`) and then `fs.rename()`d into\n * place. POSIX `rename` is atomic and replaces an existing symlink in\n * a single step, so two concurrent `github-router claude` startups can't\n * race to `EEXIST` — the loser's rename just overwrites the winner's\n * symlink with an identical one. Gemini-critic 3-lab-review finding.\n *\n * Pre-creates `~/.claude/<name>/` as a real directory if missing so\n * Claude Code's writes through the symlink don't fail with ENOENT.\n */\nasync function ensureSharedSymlink(\n name: string,\n sourceDir: string,\n mirrorDir: string,\n): Promise<void> {\n const sourcePath = path.join(sourceDir, name)\n const mirrorPath = path.join(mirrorDir, name)\n\n // 1. Ensure the source directory exists. Without this, Claude Code's\n // writes through the symlink (e.g. `projects/<hash>/foo.jsonl`)\n // fail with ENOENT on the parent dir.\n try {\n await fs.mkdir(sourcePath, { recursive: true })\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the symlink and rename catches below):\n // if the source dir cannot be created (e.g. a stray regular file\n // sitting at `~/.claude/projects`, perms blocking mkdir on a\n // corp-managed Windows box, OneDrive cloud-only reparse point),\n // ensureSharedSymlink returns without creating a junction. The\n // spawned Claude Code child then writes to the REAL `~/.claude`\n // while the proxy reads from the mirror — exactly the split-brain\n // pattern this whole function exists to prevent. Silent debug-log\n // hid this from us once already; warn so the user sees the cause.\n consola.warn(\n `ensureSharedSymlink(${name}): cannot mkdir source ${sourcePath}:`,\n err,\n )\n return\n }\n\n // 2. Inspect the mirror-side slot.\n let existing: Awaited<ReturnType<typeof fs.lstat>> | null = null\n try {\n existing = await fs.lstat(mirrorPath)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the other fs catches in this function):\n // ENOENT is the only expected-and-benign failure mode here\n // (slot doesn't exist yet — falls through to create). Any other\n // lstat failure (EACCES, ELOOP, EIO from a sketchy reparse\n // point) means we bail without creating the junction, which\n // silently leaves the proxy and child diverged. A visible warn\n // surfaces the root cause instead of a mysteriously missing\n // junction.\n consola.warn(\n `ensureSharedSymlink(${name}): cannot lstat ${mirrorPath}:`,\n err,\n )\n return\n }\n }\n\n if (existing?.isSymbolicLink()) {\n // Resolve both sides to their canonical absolute paths and compare.\n // We use `fs.realpath` rather than the raw `fs.readlink()` output\n // because Windows junctions resolve via readlink to `\\\\?\\`-prefixed\n // device-namespace paths (e.g. `\\\\?\\C:\\Users\\foo\\.claude\\projects`)\n // while we wrote the plain absolute `sourcePath` (e.g.\n // `C:\\Users\\foo\\.claude\\projects`) with `fs.symlink`. A literal\n // `===` on the raw readlink output never matched on Windows, so\n // the fast path silently failed and every startup tore down +\n // recreated all 9 SHARED junctions — masked locally because NTFS\n // File System Tunneling forges the creation timestamp for a name\n // deleted and recreated within 15 s (the per-startup churn was\n // real, the ctime-stable assertion was a false negative). The\n // realpath comparison canonicalizes both forms to the same string\n // on POSIX and Windows alike, and as a bonus handles drive-letter\n // casing / trailing-slash differences too. The extra two syscalls\n // per slot are negligible at proxy startup (runs once per launch).\n //\n // CRITICAL: sourceReal and currentReal are NOT treated symmetrically.\n // If `sourceReal` is null (we just mkdir'd it above, but realpath\n // failed — OneDrive cloud-only reparse point, EACCES on parent,\n // EXDEV mount oddity), we WARN AND RETURN rather than fall through.\n // Falling through would do unlink+symlink+rename with the same\n // failing realpath next launch — silent every-startup churn, the\n // exact bug class round-3 G2 fixed in a different code path.\n // `currentReal === null` is benign (broken/wrong slot — replace).\n const sourceReal = await fs.realpath(sourcePath).catch(() => null)\n if (sourceReal === null) {\n consola.warn(\n `ensureSharedSymlink(${name}): cannot resolve source ${sourcePath} ` +\n `— skipping junction creation to avoid silent every-startup churn. ` +\n `Inspect the source dir's permissions / OneDrive sync state and re-launch.`,\n )\n return\n }\n const currentReal = await fs.realpath(mirrorPath).catch(() => null)\n if (currentReal !== null && currentReal === sourceReal) {\n return\n }\n // Wrong target (or unresolvable mirror) — fall through to the\n // atomic-rename replace path.\n } else if (existing?.isDirectory()) {\n // Legacy real directory at the slot. Try `fs.rmdir` — on POSIX it\n // succeeds ONLY if the directory is empty, so there's nothing to\n // lose. If it's non-empty (ENOTEMPTY) or any other failure occurs,\n // fall back to the warn-and-skip path so we never auto-clobber\n // user data.\n try {\n await fs.rmdir(mirrorPath)\n // Empty dir reaped — fall through to the atomic-rename create path.\n } catch (err) {\n consola.warn(\n `ensureClaudeConfigMirror: ${mirrorPath} is a non-empty real directory ` +\n `from an older github-router version; refusing to clobber. ` +\n `If you want chat-history continuity for \"${name}\", move its ` +\n `contents into ${sourcePath}/ then delete ${mirrorPath}; the ` +\n `mirror will create a symlink (junction on Windows) on next launch. ` +\n `(rmdir error: ${(err as NodeJS.ErrnoException).code ?? \"unknown\"})`,\n )\n return\n }\n } else if (existing) {\n // Regular file (or special inode like a socket) — never auto-clobber.\n consola.warn(\n `ensureClaudeConfigMirror: ${mirrorPath} is a regular file at a ` +\n `SHARED symlink slot; refusing to clobber. Inspect and remove ` +\n `manually if safe; the mirror will create a symlink on next launch.`,\n )\n return\n }\n\n // 3. Atomic-rename creation: symlink to a unique temp path, then\n // rename over the slot. `fs.rename` replaces existing symlinks\n // atomically on POSIX and is safe against concurrent racers.\n // On Windows, MoveFileEx with MOVEFILE_REPLACE_EXISTING does NOT\n // replace an existing directory or junction destination\n // (npm/cli#9021), so when the slot already holds a wrong-target\n // junction we must explicitly unlink it first. The sub-millisecond\n // window of no-link is acceptable: ensureClaudeConfigMirror is\n // idempotent under concurrency and only runs at proxy startup,\n // before any spawned Claude Code child has been launched.\n const tempPath = `${mirrorPath}.tmp.${process.pid}.${randomBytes(4).toString(\"hex\")}`\n try {\n await fs.symlink(\n sourcePath,\n tempPath,\n process.platform === \"win32\" ? \"junction\" : \"dir\",\n )\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\" rule:\n // the rule applies to ALL fs catches in this function, not just the\n // rename one. The temp path is per-pid + 8-hex random so EEXIST is\n // essentially impossible — any failure here (EPERM on Windows\n // without DevMode, EXDEV cross-volume, ENOSPC, …) is a real\n // operational problem the user needs to see.\n consola.warn(\n `ensureSharedSymlink(${name}): symlink ${tempPath} failed:`,\n err,\n )\n return\n }\n if (process.platform === \"win32\" && existing?.isSymbolicLink()) {\n // Windows-only: clear the wrong-target junction so the rename\n // below can land. Best-effort — if a concurrent racer already\n // unlinked it, the rename succeeds as a CREATE; if a concurrent\n // racer already replaced it with a fresh junction, the rename\n // hits the catch below and we surface a warn.\n await fs.unlink(mirrorPath).catch(() => {})\n }\n try {\n await fs.rename(tempPath, mirrorPath)\n } catch (err) {\n // Escalated from debug → warn per the CLAUDE.md \"smoking gun\"\n // rule (consistent with the fs.symlink catch above): a silent\n // debug log here previously hid the Windows rename-replace bug\n // (junction-over-junction MoveFileEx EPERM). Post-fix, rename\n // failures should be rare and visible.\n consola.warn(\n `ensureSharedSymlink(${name}): rename ${tempPath} → ${mirrorPath} failed:`,\n err,\n )\n await fs.unlink(tempPath).catch(() => {})\n }\n}\n\nasync function ensureFile(filePath: string): Promise<void> {\n try {\n await fs.access(filePath, fs.constants.W_OK)\n } catch {\n await fs.writeFile(filePath, \"\")\n await fs.chmod(filePath, 0o600)\n }\n}\n\nasync function chmodIfPossible(target: string, mode: number): Promise<void> {\n if (process.platform === \"win32\") return // Windows chmod is no-op-ish\n try {\n await fs.chmod(target, mode)\n } catch (err) {\n consola.debug(`chmod ${target} ${mode.toString(8)} failed:`, err)\n }\n}\n\n/**\n * Write a runtime tempfile securely.\n *\n * - Mode `0o600` so other local users (multi-tenant boxes, shared\n * dev containers) can't read the per-launch nonce or runtime URL.\n * - `flag: \"wx\"` (O_CREAT | O_EXCL | O_WRONLY) refuses to overwrite\n * an existing path. POSIX open(2) with O_EXCL also rejects\n * pre-placed symlinks, killing the symlink-clobber attack vector.\n * - The caller's responsibility to pick a path NOT yet in use.\n * We intentionally do NOT pre-unlink: an `lstat` + `unlink` +\n * `open(O_EXCL)` sequence still has a TOCTOU window where an\n * attacker can drop a symlink between unlink and open. Letting\n * `wx` fail is the safer behavior — surfaces the conflict\n * instead of silently following.\n */\nexport async function writeRuntimeFileSecure(\n filePath: string,\n content: string,\n): Promise<void> {\n await fs.writeFile(filePath, content, { mode: 0o600, flag: \"wx\" })\n}\n\n/**\n * Sweep stale runtime tempfiles. Removes files whose embedded PID is no\n * longer a live process. A proxy crash (`kill -9`, OS reboot) leaves\n * orphans that would otherwise accumulate forever — and worse, a stale\n * config pointing at a now-recycled port could route MCP traffic to\n * whatever process bound that port next.\n *\n * Naming convention: `peer-mcp-<pid>.json` and `peer-agents-<pid>.json`.\n * Files not matching either pattern are left alone — this directory\n * is shared with future runtime artifacts.\n *\n * We deliberately do NOT age-prune files whose PID is alive. A\n * legitimately long-running proxy can have a tempfile older than any\n * arbitrary threshold; deleting it out from under the live process\n * breaks the spawned Claude Code child's MCP/agent wiring with no clean\n * recovery. PID-wraparound risk is mitigated by (a) PID reuse on Linux\n * being slow under typical loads, and (b) the file is only consulted by\n * github-router itself — an unrelated process that inherits the PID\n * never reads it.\n */\nexport async function sweepStaleRuntimeFiles(): Promise<void> {\n const dir = PATHS.CLAUDE_RUNTIME_DIR\n let entries: Array<string>\n try {\n entries = await fs.readdir(dir)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n\n for (const name of entries) {\n // Match both legacy `peer-mcp-<pid>.json` and current\n // `peer-mcp-<pid>-<rand>.json` filenames so we can clean up either.\n const match = /^peer-(?:mcp|agents)-(\\d+)(?:-[0-9a-f]+)?\\.json$/.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n const filePath = path.join(dir, name)\n\n if (isPidAlive(pid)) continue\n\n await fs.unlink(filePath).catch(() => {\n // already gone or unreadable, fine\n })\n }\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n // signal 0 = check existence without delivering a signal. EPERM\n // means the process exists but we can't signal it (which is still\n // \"alive\" for our purposes); ESRCH means it's gone.\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Sweep stale peer-* subagent .md files from the router-owned\n * `CLAUDE_CONFIG_DIR/agents/`. Phase 2.5 writes one .md per peer agent\n * into Claude Code's agents directory (now our config dir's `agents/`\n * subdir, since `getClaudeCodeEnvVars` points `CLAUDE_CONFIG_DIR` at\n * `PATHS.CLAUDE_CONFIG_DIR`) so they appear in Claude Code's Task\n * `subagent_type` enum. Files are named `peer-<pid>-<rand>-<agentName>.md`\n * so this sweep can drop orphans from crashed prior proxy sessions\n * without touching the user's own .md files (which were copied into\n * the same dir during `ensureClaudeConfigMirror`).\n *\n * Same liveness rule as `sweepStaleRuntimeFiles`: only delete when the\n * file's embedded PID is no longer alive. Live PIDs keep their files —\n * a long-running proxy doesn't lose its agent registrations.\n *\n * Regex tightening (Phase 2.6, codex-critic + gemini-critic 2-lab finding):\n * the original sweep regex `^peer-(\\d+)(?:-[0-9a-f]+)?-.+\\.md$` was too\n * permissive — a user-authored `peer-12345-meeting-notes.md` matches\n * (`12345` = \"PID\", `-meeting-notes` = trailing `.+`) and would be\n * silently unlinked when 12345 happens to be a dead PID (overwhelmingly\n * likely). Tightened to require BOTH the 8-hex-char random suffix AND\n * an exact-match persona name suffix, eliminating the risk for any\n * realistic user filename.\n */\nexport async function sweepStalePeerAgentMdFiles(): Promise<void> {\n const dir = path.join(PATHS.CLAUDE_CONFIG_DIR, \"agents\")\n let entries: Array<string>\n try {\n entries = await fs.readdir(dir)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n for (const name of entries) {\n const match = PEER_AGENT_MD_FILENAME.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n if (isPidAlive(pid)) continue\n await fs.unlink(path.join(dir, name)).catch(() => {\n // already gone or unreadable, fine\n })\n }\n}\n\n/**\n * Strict regex matching only files this proxy writes:\n * peer-<pid>-<8 hex>-<exact persona/coordinator name>.md\n * The persona-name allowlist is the load-bearing protection against\n * deleting user files. Update this list whenever a new persona is added\n * to `PERSONAS_READ` / `PERSONAS_WRITE` in `peer-mcp-personas.ts` or a\n * new coordinator-style agent is added in `codex-mcp-config.ts`.\n */\nconst PEER_AGENT_MD_FILENAME =\n /^peer-(\\d+)-[0-9a-f]{8}-(?:codex-critic|codex-reviewer|gemini-critic|codex-implementer|peer-review-coordinator)\\.md$/\n\n/**\n * Strict regex matching only per-launch claude-config mirror dirs this\n * proxy creates: `<pid>-<8 hex>`. Anchored to the entire entry name so\n * user-authored siblings under `<appDir>/claude-config/` (if any) are\n * untouchable. The PID prefix is what `sweepStaleClaudeConfigMirrors`\n * keys off; the 8-hex random suffix matches `randomBytes(4)` exactly\n * (no `?` — files created by a different shape are not ours).\n */\nconst CLAUDE_CONFIG_MIRROR_DIR = /^(\\d+)-[0-9a-f]{8}$/\n\n/**\n * Sweep stale per-launch CLAUDE_CONFIG_DIR mirrors left behind by\n * crashed prior proxy sessions. Symmetric to `sweepStalePeerAgentMdFiles`\n * — same liveness rule (only delete when the embedded PID is dead),\n * same strict regex (the dir-name allowlist is the load-bearing\n * protection against deleting user-authored siblings).\n *\n * Scans `<appDir>/claude-config/` (the parent of the per-launch dirs).\n * Each entry whose name matches `<pid>-<8 hex>` AND whose PID is no\n * longer alive is removed recursively. `fs.rm({recursive: true})`\n * walks the tree calling `unlink` on symlinks/junctions rather than\n * following them, so the SHARED junctions back to `~/.claude/<X>`\n * are removed without touching their targets.\n *\n * Tolerates missing parent dir (first-ever launch, or user wiped it).\n */\nexport async function sweepStaleClaudeConfigMirrors(): Promise<void> {\n const parent = path.join(appDir(), \"claude-config\")\n let entries: Array<string>\n try {\n entries = await fs.readdir(parent)\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return\n throw err\n }\n for (const name of entries) {\n const match = CLAUDE_CONFIG_MIRROR_DIR.exec(name)\n if (!match) continue\n const pid = Number.parseInt(match[1], 10)\n if (isPidAlive(pid)) continue\n await fs\n .rm(path.join(parent, name), { recursive: true, force: true })\n .catch((err) => {\n // Best-effort: stale-dir cleanup must never block startup.\n // Common failure modes (worth surviving silently): an EBUSY/EPERM\n // on Windows if a leftover handle is still open, or a stray\n // root-owned file inside the dir from a previous run with\n // different permissions.\n consola.debug(\n `sweepStaleClaudeConfigMirrors: cannot rm ${name}:`,\n err,\n )\n })\n }\n}\n\n/**\n * Remove THIS launch's per-launch CLAUDE_CONFIG_DIR on shutdown.\n * Best-effort: a failure here must not block process exit (the caller\n * wraps this in a `.catch`-equivalent via `launchChild`'s onShutdown\n * try/catch). Symmetric to `writePeerMcpRuntimeFiles`'s `cleanup()`:\n * we own this dir for the lifetime of the proxy, so removing it on\n * normal shutdown is correct; the boot-time sweep handles the\n * abnormal-exit case.\n *\n * `fs.rm({recursive: true})` removes SHARED junctions via unlink\n * (does NOT follow them into the user's real `~/.claude/<X>`).\n */\nexport async function removeOwnClaudeConfigMirror(): Promise<void> {\n const dir = PATHS.CLAUDE_CONFIG_DIR\n await fs.rm(dir, { recursive: true, force: true }).catch((err) => {\n consola.debug(`removeOwnClaudeConfigMirror: rm ${dir} skipped:`, err)\n })\n}\n"],"mappings":"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"}