Summary
Add the ability to define named environment variable masks that are overlaid on child process environments before exec. Masks are data (key-value maps), not shell scripts — no sourcing, no subshell expansion, no mutation of the Claude Code parent process. This applies to all child processes Claude Code spawns (Bash tool, MCP servers, hooks, any forked process), not just shell invocations.
Problem
Claude Code spawns child processes for tool calls, MCP servers, and hooks. Environment variables set in one child don't persist to the next. The existing CLAUDE_ENV_FILE mechanism (#15840, #27987) relies on file sourcing with shell evaluation semantics, which is fragile and currently broken.
Certain workflows require consistent environment setup across many child processes (JDK paths, timezone overrides, ulimits, registry URLs). Today, every invocation must redundantly inline these values, or wrap commands in subshells with dynamic evaluation like $(ulimit -Hn) — which triggers command approval prompts and adds noise.
Proposal
Define named masks
A mask is a named key-value map, defined once per session or persisted in config:
EnvMask("gradle", {
"ULIMIT_HN": "1048576",
"JAVA_HOME": "/usr/lib/jvm/java-17-openjdk-amd64",
"TZ": "America/Los_Angeles"
})
EnvMask("node", {
"NODE_OPTIONS": "--max-old-space-size=4096",
"NPM_CONFIG_REGISTRY": "https://registry.internal/npm"
})
EnvMask("base", {
"TZ": "America/Los_Angeles"
})
Apply by reference
Child processes specify which mask(s) to apply. The mask is overlaid on the inherited environment in the fork-before-exec path — before the child process starts. No tool interface changes are required; this happens at the process spawning layer inside Claude Code.
Ordered composition
Multiple masks can be applied as a named series with clear left-to-right precedence. Later masks override earlier ones on key collision:
env_mask: ["base", "gradle"]
This allows a common base (timezone, locale) to be shared without duplicating values in each mask. Specialized masks override only what they need to.
Persistence
Masks defined in session are ephemeral. Masks defined in config (e.g., settings.json or a dedicated masks file) persist across sessions and are available by identifier without re-declaration.
Implementation notes
- Masks are applied at the process spawning layer, after fork and before exec. The parent Claude Code process environment is never modified.
- Masks are pure data — no shell expansion, no eval, no sourcing. Values are literal strings.
- Because this operates at the fork/exec boundary, it applies uniformly to all child processes regardless of type (shell, MCP server, hook, etc.). No per-tool integration is needed.
- This replaces the need for
CLAUDE_ENV_FILE sourcing and avoids the class of bugs around shell evaluation in env setup.
Related issues
Summary
Add the ability to define named environment variable masks that are overlaid on child process environments before exec. Masks are data (key-value maps), not shell scripts — no sourcing, no subshell expansion, no mutation of the Claude Code parent process. This applies to all child processes Claude Code spawns (Bash tool, MCP servers, hooks, any forked process), not just shell invocations.
Problem
Claude Code spawns child processes for tool calls, MCP servers, and hooks. Environment variables set in one child don't persist to the next. The existing
CLAUDE_ENV_FILEmechanism (#15840, #27987) relies on file sourcing with shell evaluation semantics, which is fragile and currently broken.Certain workflows require consistent environment setup across many child processes (JDK paths, timezone overrides, ulimits, registry URLs). Today, every invocation must redundantly inline these values, or wrap commands in subshells with dynamic evaluation like
$(ulimit -Hn)— which triggers command approval prompts and adds noise.Proposal
Define named masks
A mask is a named key-value map, defined once per session or persisted in config:
Apply by reference
Child processes specify which mask(s) to apply. The mask is overlaid on the inherited environment in the fork-before-exec path — before the child process starts. No tool interface changes are required; this happens at the process spawning layer inside Claude Code.
Ordered composition
Multiple masks can be applied as a named series with clear left-to-right precedence. Later masks override earlier ones on key collision:
This allows a common base (timezone, locale) to be shared without duplicating values in each mask. Specialized masks override only what they need to.
Persistence
Masks defined in session are ephemeral. Masks defined in config (e.g.,
settings.jsonor a dedicated masks file) persist across sessions and are available by identifier without re-declaration.Implementation notes
CLAUDE_ENV_FILEsourcing and avoids the class of bugs around shell evaluation in env setup.Related issues