From 9799593abb4ae8363e9482e81eaaf20fbaa8d389 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 12 Jun 2026 11:17:19 -0700 Subject: [PATCH] feat: PTY runner spawns hermetic claude sessions launchClaudePty children get the allowlist-scrubbed env, a gated --strict-mcp-config, and the session exposes hermeticConfigDir for forensics (hermetic plan files live under /plans/ and still match extractPlanFilePath via the /.claude dir-name contract). Seeded trust state covers repo-cwd sessions; the 15s trust-watcher stays as fallback. Verified foreground via the plan-mode-no-op gate test. Co-Authored-By: Claude Fable 5 --- test/helpers/claude-pty-runner.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/helpers/claude-pty-runner.ts b/test/helpers/claude-pty-runner.ts index f06ba05f3..a371af866 100644 --- a/test/helpers/claude-pty-runner.ts +++ b/test/helpers/claude-pty-runner.ts @@ -24,6 +24,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { hermeticChildEnv, isHermeticEnabled } from './hermetic-env'; /** Strip ANSI escapes for pattern-matching against visible text. */ export function stripAnsi(s: string): string { @@ -120,6 +121,13 @@ export interface ClaudePtySession { exited(): boolean; /** Exit code, if known. */ exitCode(): number | null; + /** + * The hermetic CLAUDE_CONFIG_DIR this session's claude was pointed at, or + * null when EVALS_HERMETIC=0. Forensics: hermetic plan files live under + * `/plans/` (extractPlanFilePath still matches them — + * the dir name ends in `/.claude` by contract). + */ + hermeticConfigDir: string | null; /** * Send SIGINT, then SIGKILL after 1s. Always safe to call multiple times. * Awaits process exit before resolving. @@ -1143,8 +1151,17 @@ export async function launchClaudePty( if (permissionMode !== null) { args.push('--permission-mode', permissionMode); } + // Hermetic children get zero MCP servers; gated on the same call-time + // check as the env scrub so EVALS_HERMETIC=0 restores operator MCP too. + // Before opts.extraArgs so a test could theoretically supply --mcp-config. + const hermetic = isHermeticEnabled(); + if (hermetic) args.push('--strict-mcp-config'); if (opts.extraArgs) args.push(...opts.extraArgs); + // Hermetic by default (test/helpers/hermetic-env.ts): operator session + // context never reaches the child; per-test opts.env merges last. + const childEnv = hermeticChildEnv(opts.env); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const proc = (Bun as any).spawn([claudePath, ...args], { terminal: { @@ -1155,7 +1172,7 @@ export async function launchClaudePty( }, }, cwd, - env: { ...process.env, ...(opts.env ?? {}) }, + env: childEnv, }); // Track exit so waitForAny can fail fast if claude crashes. @@ -1307,6 +1324,7 @@ export async function launchClaudePty( pid: () => proc.pid as number | undefined, exited: () => exited, exitCode: () => exitCodeCaptured, + hermeticConfigDir: hermetic ? childEnv.CLAUDE_CONFIG_DIR ?? null : null, close, }; }