[Windows] Bash tool permanently wedged: hook env (CLAUDE_ENV_FILE) grows on every compact, torn export line fails every command with "line 182: e: command not found" (exit 127)

Status Open
Reported on v2.1.211
Maintainer reply None cached
Activity 0 comments · opened Jul 16, 2026

Summary

On Windows (VSCode extension), the Bash tool inlines the accumulated SessionStart-hook env file (CLAUDE_ENV_FILE) into every command's bash -c preamble without any validation, deduplication, or truncation. In long sessions this block grows unboundedly (hooks re-run and re-append on every compact), and one torn/corrupted export line — produced while two hook processes appended concurrently — permanently wedged the session: every subsequent Bash call (even echo ok) failed with:

/usr/bin/bash: line 182: e: command not found
Exit code 127

e / exp are fragments of the word export. The poisoned env script is cached per session, so the session never recovers; resuming the conversation reloads the corrupted content (error moved to line 190 after a full VSCode restart + resume). Only a brand-new conversation is clean.

Environment

  • Claude Code VSCode extension 2.1.211 (native binary), Windows 10 Pro 10.0.19045, Git Bash (/usr/bin/bash MSYS2)
  • SessionStart hooks writing env via CLAUDE_ENV_FILE (matcher startup|resume|clear|compact): one user hook writing ~33 export lines with separate appendFileSync calls, plus a plugin hook (openai-codex plugin) writing 2–3 more lines to the same mechanism

Evidence (from Claude VSCode.log, window 20260716T040938)

The env script size logged by the loader grows in a staircase over a ~2h session — one extra copy of the full export block appended per compact/hook event:

11:10:14  Session environment script ready (1508 chars total)
11:36:40  ... (3071 chars total)
12:15:04  ... (4526 chars total)
12:51:58  ... (6197 chars total)   ← then alternates 6197/6089 (two competing hook-file states)
13:03:52  ... (9215 chars total)   ← first failing spawn, exit 127, "line 182: e: command not found"

At the failing spawn:

13:03:51.152  File C:\Users\Leona\.claude.json written atomically
13:03:52.731  Hook output does not start with {, treating as plain text
13:03:52.747  Hook output does not start with {, treating as plain text   ← two hook processes running
13:03:52.770  [Stall] tool_dispatch_start tool=Bash ...
13:03:52.806  Session environment loaded from 2 hook file(s)
13:03:52.806  Session environment script ready (9215 chars total)          ← +3KB jump, torn content
13:03:53.034  [Stall] tool_dispatch_end tool=Bash outcome=error

9215 chars ≈ 190 lines — matching the reported failure lines 182/190. A healthy fresh session's preamble is ~35 lines (verified by dumping /proc/$$/cmdline from inside the Bash tool).

Notes:

  • The last successful Bash call was 20 seconds earlier (13:03:32). No user config changed in between.
  • Subagents spawned from the wedged session inherited the poisoned env and failed identically.
  • bash -lc invoked directly via spawnSync on the same machine worked perfectly throughout — the corruption is entirely inside the constructed preamble.

Failure-shape reproduction

A 180-line export block containing one region with an unbalanced quote plus a stranded fragment reproduces the exact user-visible shape — single error, nothing else executes, exit 127:

$ bash repro.sh        # 175 lines of export F<i>="x", then:
                       #   export BAD="abc
                       #   export MID="swallowed
                       #   e "
                       #   eval 'echo SHOULD_NOT_PRINT'
                       #   "
repro.sh: line 180: e: command not found
$ echo $?
127

Root cause (three compounding defects)

  1. Unbounded growth: SessionStart hooks re-run on compact/resume and append to the session env; nothing truncates or dedupes by key, so the preamble grows ~1.5KB per compact.
  2. No write/read atomicity: multiple hook processes append to the same mechanism with many small appendFileSync calls while the loader reads — a torn line is possible (and observed: e/exp = torn export).
  3. No validation before inlining: the loader inlines the script into bash -c verbatim and caches it per session. One bad line (unbalanced quote) silently swallows the rest of the preamble including the user command, and the cache makes it permanent for the session; resume reloads it.

Suggested fixes (any one of these would prevent the wedge)

  • Validate each env line (export KEY="value" shape, balanced quoting) before inlining; drop/log malformed lines instead of poisoning every command.
  • Deduplicate by key when rebuilding the session env script (also fixes unbounded preamble growth on compact).
  • bash -n the assembled preamble once after rebuild; on failure, rebuild from last-known-good instead of caching the poisoned script.

Workarounds we applied (user side)

  • Hook now skips env writes when source === "compact" and batches all exports into a single atomic append — no recurrence since.
  • Recovery when wedged: start a new conversation (env script cache is per-session). VSCode restart + resume does not help.

Related

  • #27987 (CLAUDE_ENV_FILE not sourced on Windows), #24775 (env file session-dir mismatch on resume; loader caching), #15840 (CLAUDE_ENV_FILE empty) — adjacent env-file mechanism issues, none cover this corruption/wedge.

View original on GitHub ↗