Skip to content

Fix #281: implement the CLAUDE_ENV_FILE source-and-apply cycle#313

Open
ericleepi314 wants to merge 1 commit into
fix/issue-280-appstate-persistencefrom
fix/issue-281-claude-env-file
Open

Fix #281: implement the CLAUDE_ENV_FILE source-and-apply cycle#313
ericleepi314 wants to merge 1 commit into
fix/issue-280-appstate-persistencefrom
fix/issue-281-claude-env-file

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

Closes #281

Stacked on #312 (deep stack down to #304). Merge in order; GitHub retargets automatically.

Summary

CLAUDE_ENV_FILE was set for SessionStart/Setup/CwdChanged hooks but the documented contract — write export FOO=bar to the file, subsequent Bash tool calls see FOO — was unimplemented. It was doubly broken: nothing read the file back, and the parent directory was never created so the hook's redirect failed outright.

Design (shaped heavily by critic review against the TS source)

  • Shell-evaluated, not parsed: the file is sourced by /bin/sh -c 'set -a; . file; env -0' and the env delta is captured. This is load-bearing — the canonical idiom export PATH="$HOME/bin:$PATH" (venv/conda activation) must expand; the first-draft literal parser would have set PATH to the literal string and bricked every subsequent spawn. Pinned by test_path_prepend_does_not_brick_bash_spawn (real spawn resolution through the modified PATH).
  • Bash-tool-only scope (TS sessionEnvironment.ts parity): exports land in a session-scoped dict (src/hooks/session_env.py, leaf module) merged over os.environ at Bash tool spawn — foreground and background. The host process env (provider SDK, MCP spawns) is never touched.
  • Per-event buckets with TS HOOK_ENV_PRIORITY precedence (Setup < SessionStart < CwdChanged). Each fire replaces its event's bucket — on both dispatch paths (_run_hooks_for_event and the run_session_start_hooks router) — so a cwd change drops the previous directory's exports (TS clearCwdEnvFiles semantics), and a SessionStart re-fire can't accumulate.
  • Apply on success only (exit 0): documented divergence from TS, which sources unconditionally — partial writes from failed/timed-out hooks are discarded. Ephemeral file removed on every exit path.
  • PowerShell hooks get no env file (TS !isPowerShell parity). The env dict is built once per fire so the apply step reads the exact path the hook wrote (the previous per-branch build would have minted two different paths).

Test plan

  • 32 tests: shell-eval semantics ($VAR expansion, quoting, set -a, noise-key exclusion, chained prepends, failed source), bucket semantics (precedence incl. the divergent Setup/SessionStart pair, CwdChanged replace, both dispatch paths), full e2e (real hook subprocess → session dict → Bash tool dispatch sees the var), failed-hook discard, non-lifecycle exclusion, PowerShell exclusion, host-env-untouched
  • All 542 hook+bash tests pass; conftest-wide bucket reset keeps the suite hermetic
  • Full suite on the stack: 7833 passed, 0 failed, 5 skipped
  • Critic review loop: APPROVE after 2 revision rounds (the PATH-bricking literal parse, os.environ scope overshoot, and dual-dispatch-path clearing all came out of it, verified against typescript/src/utils/sessionEnvironment.ts)

🤖 Generated with Claude Code

The executor set CLAUDE_ENV_FILE for SessionStart/Setup/CwdChanged
hooks but nothing read it back — and the parent directory was never
created, so the documented `echo export... > "$CLAUDE_ENV_FILE"`
contract was doubly broken.

- exports are SHELL-EVALUATED (sh -c 'set -a; . file; env -0') and
  diffed against the session baseline: export PATH="$HOME/bin:$PATH"
  expands instead of bricking every later spawn with a literal string;
  chained prepends compose because the baseline includes prior exports
- scope matches TS sessionEnvironment.ts: exports land in a session
  dict (src/hooks/session_env.py) merged over os.environ at BASH TOOL
  spawn (foreground + background) — the host process env is untouched
- per-event buckets with TS HOOK_ENV_PRIORITY precedence
  (Setup < SessionStart < CwdChanged); each fire REPLACES its event's
  bucket on both dispatch paths, so a cwd change drops the previous
  directory's exports (TS clearCwdEnvFiles)
- only a hook that exits 0 gets its exports applied (documented
  divergence from TS, which sources unconditionally); the ephemeral
  file is discarded on every exit path
- PowerShell hooks get no env file (TS !isPowerShell parity); env
  built once per fire so the apply step reads the same path the hook
  wrote; conftest-wide bucket reset keeps tests hermetic

Closes #281

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant