/ src / utils / subprocessEnv.ts
subprocessEnv.ts
 1  import { isEnvTruthy } from './envUtils.js'
 2  
 3  /**
 4   * Env vars to strip from subprocess environments when running inside GitHub
 5   * Actions. This prevents prompt-injection attacks from exfiltrating secrets
 6   * via shell expansion (e.g., ${ANTHROPIC_API_KEY}) in Bash tool commands.
 7   *
 8   * The parent claude process keeps these vars (needed for API calls, lazy
 9   * credential reads). Only child processes (bash, shell snapshot, MCP stdio, LSP, hooks) are scrubbed.
10   *
11   * GITHUB_TOKEN / GH_TOKEN are intentionally NOT scrubbed — wrapper scripts
12   * (gh.sh) need them to call the GitHub API. That token is job-scoped and
13   * expires when the workflow ends.
14   */
15  const GHA_SUBPROCESS_SCRUB = [
16    // Anthropic auth — claude re-reads these per-request, subprocesses don't need them
17    'ANTHROPIC_API_KEY',
18    'CLAUDE_CODE_OAUTH_TOKEN',
19    'ANTHROPIC_AUTH_TOKEN',
20    'ANTHROPIC_FOUNDRY_API_KEY',
21    'ANTHROPIC_CUSTOM_HEADERS',
22  
23    // OTLP exporter headers — documented to carry Authorization=Bearer tokens
24    // for monitoring backends; read in-process by OTEL SDK, subprocesses never need them
25    'OTEL_EXPORTER_OTLP_HEADERS',
26    'OTEL_EXPORTER_OTLP_LOGS_HEADERS',
27    'OTEL_EXPORTER_OTLP_METRICS_HEADERS',
28    'OTEL_EXPORTER_OTLP_TRACES_HEADERS',
29  
30    // Cloud provider creds — same pattern (lazy SDK reads)
31    'AWS_SECRET_ACCESS_KEY',
32    'AWS_SESSION_TOKEN',
33    'AWS_BEARER_TOKEN_BEDROCK',
34    'GOOGLE_APPLICATION_CREDENTIALS',
35    'AZURE_CLIENT_SECRET',
36    'AZURE_CLIENT_CERTIFICATE_PATH',
37  
38    // GitHub Actions OIDC — consumed by the action's JS before claude spawns;
39    // leaking these allows minting an App installation token → repo takeover
40    'ACTIONS_ID_TOKEN_REQUEST_TOKEN',
41    'ACTIONS_ID_TOKEN_REQUEST_URL',
42  
43    // GitHub Actions artifact/cache API — cache poisoning → supply-chain pivot
44    'ACTIONS_RUNTIME_TOKEN',
45    'ACTIONS_RUNTIME_URL',
46  
47    // claude-code-action-specific duplicates — action JS consumes these during
48    // prepare, before spawning claude. ALL_INPUTS contains anthropic_api_key as JSON.
49    'ALL_INPUTS',
50    'OVERRIDE_GITHUB_TOKEN',
51    'DEFAULT_WORKFLOW_TOKEN',
52    'SSH_SIGNING_KEY',
53  ] as const
54  
55  /**
56   * Returns a copy of process.env with sensitive secrets stripped, for use when
57   * spawning subprocesses (Bash tool, shell snapshot, MCP stdio servers, LSP
58   * servers, shell hooks).
59   *
60   * Gated on CLAUDE_CODE_SUBPROCESS_ENV_SCRUB. claude-code-action sets this
61   * automatically when `allowed_non_write_users` is configured — the flag that
62   * exposes a workflow to untrusted content (prompt injection surface).
63   */
64  // Registered by init.ts after the upstreamproxy module is dynamically imported
65  // in CCR sessions. Stays undefined in non-CCR startups so we never pull in the
66  // upstreamproxy module graph (upstreamproxy.ts + relay.ts) via a static import.
67  let _getUpstreamProxyEnv: (() => Record<string, string>) | undefined
68  
69  /**
70   * Called from init.ts to wire up the proxy env function after the upstreamproxy
71   * module has been lazily loaded. Must be called before any subprocess is spawned.
72   */
73  export function registerUpstreamProxyEnvFn(
74    fn: () => Record<string, string>,
75  ): void {
76    _getUpstreamProxyEnv = fn
77  }
78  
79  export function subprocessEnv(): NodeJS.ProcessEnv {
80    // CCR upstreamproxy: inject HTTPS_PROXY + CA bundle vars so curl/gh/python
81    // in agent subprocesses route through the local relay. Returns {} when the
82    // proxy is disabled or not registered (non-CCR), so this is a no-op outside
83    // CCR containers.
84    const proxyEnv = _getUpstreamProxyEnv?.() ?? {}
85  
86    if (!isEnvTruthy(process.env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB)) {
87      return Object.keys(proxyEnv).length > 0
88        ? { ...process.env, ...proxyEnv }
89        : process.env
90    }
91    const env = { ...process.env, ...proxyEnv }
92    for (const k of GHA_SUBPROCESS_SCRUB) {
93      delete env[k]
94      // GitHub Actions auto-creates INPUT_<NAME> for `with:` inputs, duplicating
95      // secrets like INPUT_ANTHROPIC_API_KEY. No-op for vars that aren't action inputs.
96      delete env[`INPUT_${k}`]
97    }
98    return env
99  }