Windows: Claude Code sets FILE_ATTRIBUTE_READONLY on its own ~/.claude subdirs, breaking Bash tool (EEXIST on session-env mkdir)

Resolved 💬 2 comments Opened Jun 4, 2026 by stgirat-dev Closed Jun 7, 2026

Summary

On Windows, the running Claude Code process periodically (about every 10 seconds) applies the read-only attribute (FILE_ATTRIBUTE_READONLY) to newly created subdirectories under ~/.claude. This breaks the Bash tool: when a SessionStart hook emits environment variables, the per-session directory ~/.claude/session-env/<session-id>/ is created (to host $CLAUDE_ENV_FILE) and then marked read-only. The Bash tool's own setup then tries to mkdir that same directory non-idempotently and fails with:

EEXIST: file already exists, mkdir 'C:\Users\<user>\.claude\session-env\<session-id>'

The Bash tool is then unusable for the entire session (surfaces as a "Bash tool setup problem / session-env mkdir" error).

Environment

  • Claude Code: 2.1.162 (npm global install; also present as the VS Code extension build)
  • Model: Opus 4.8 (1M context)
  • OS: Windows 11 Home (build 26200)
  • Shells: Git Bash / PowerShell

Impact

  • Bash tool dead for the whole session. Frequency varies (intermittent to almost always) because it is a race (see below).
  • Self-inflicted: Claude Code's own read-only marking breaks Claude Code's own Bash tool.

Root cause (verified on the affected machine)

Two factors combine:

  1. A hook that emits env vars at SessionStart causes the harness to pre-create ~/.claude/session-env/<session-id>/ (to host $CLAUDE_ENV_FILE) before the Bash tool initializes it. (Reproduced here with the Codex Companion plugin, whose session-lifecycle-hook.mjs appends to $CLAUDE_ENV_FILE.)
  2. The running Claude Code process sets FILE_ATTRIBUTE_READONLY on that directory within ~10 s of its creation. On a read-only-attributed, already-existing directory, the Bash tool's mkdir throws EEXIST instead of tolerating it.

It is a race: if the Bash tool creates/initializes the directory before the read-only sweep, it works; if the sweep wins, EEXIST.

How we proved Claude Code itself is the source

Controlled experiment:

  • New directories created outside ~/.claude (under %USERPROFILE% or %TEMP%) never become read-only. Only new subdirs under ~/.claude do.
  • Immediately after creation a directory is not read-only; it flips to read-only after ~10–13 s, consistently → a periodic background timer.
  • Process-of-elimination via NtSuspendProcess: suspending the Claude Desktop app, all other Claude Code sessions, and all plugin/MCP/companion node processes (41 processes total) did not stop the read-only marking. Suspending everything except the single Claude Code session's own process left the behavior intact → the marking originates from the Claude Code process itself.
  • Ruled out: antivirus (only Windows Defender, Controlled Folder Access disabled), third-party backup/sync (none installed), OS default behavior, and user configuration (no such instruction exists).

Whether the responsible code is in Claude Code core or an in-process plugin could not be separated by process suspension (plugins run in-process). Enabled plugins on this machine: superpowers, codex (openai-codex), vercel, context7, github, feature-dev, playwright, claude-md-management, skill-creator, security-guidance, claude-code-setup, playground, code-simplifier.

Reproduction

  1. Windows, Claude Code 2.1.162.
  2. Have at least one SessionStart hook that emits env vars (e.g. install the Codex Companion plugin, which appends to $CLAUDE_ENV_FILE).
  3. Start a session and create a directory under ~/.claude; observe it gains the read-only attribute within ~10 s.
  4. The Bash tool fails on first use with the EEXIST error shown above.

Suggested fixes

  • Make the Bash tool's session-env directory creation idempotent — treat an existing directory as success (e.g. fs.mkdirSync(dir, { recursive: true }) and do not throw EEXIST when the path is already a directory). This alone fixes the crash.
  • Find and remove whatever sets FILE_ATTRIBUTE_READONLY on ~/.claude subdirectories — Claude Code should never read-only-mark its own working directories.

Workaround (confirmed working)

A PreToolUse hook (matcher Bash) that strips the read-only attribute from ~/.claude/session-env before every Bash call reliably prevents the EEXIST:

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "command": "python -c \"import os,subprocess; b=os.path.join(os.path.expanduser('~'),'.claude','session-env'); subprocess.run(['attrib','-R',os.path.join(b,'*'),'/S','/D'],capture_output=True) if os.path.isdir(b) else None\"",
      "timeout": 10
    }
  ]
}

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗