[BUG] Write/Edit tools surface EBADF or ENOENT on Windows Controlled Folder Access protected paths (two error codes, one root cause)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Class B — Owner submission packet (v2 — sharpened with multi-agent corpus)
Target: new issue at https://github.com/anthropics/claude-code/issues/new
Re-rolled 2026-04-27 to fold in Archivist's CFA probe-matrix + Manager's 4th-operator data point + Tyr's 5th-operator data point + sharpened "two error codes, one root cause" framing.
Suggested title
Write/Edit tools surface EBADF or ENOENT on Windows Controlled Folder Access protected paths (two error codes, one root cause)
Suggested labels (if permitted)
bug, windows, error-handling, area:tools
Duplicate-check (done 2026-04-27 — clear to submit)
Searched anthropics/claude-code issues across 4 angles: EBADF, Controlled Folder Access, MpFilter, bad file descriptor. No matches on this class. Existing EBADF issues are on Cowork rootfs.vhdx rename / Claude Desktop git-worktrees.json renameSync / Git Bash stdout pipe — different code paths, different syscalls.
Pre-submission verification (Owner checklist before paste)
| # | Item | Verified |
|---|---|---|
| 1 | No username (repom) anywhere in issue body | — verify |
| 2 | No internal project / agent / workspace names in issue body | — verify |
| 3 | No internal rule numbers / session numbers in issue body | — verify |
| 4 | All paths generic / placeholder | — verify |
| 5 | Issue body length reasonable (~200-280 rendered lines) | — verify |
| 6 | First-paragraph summary accurate + actionable | — verify |
| 7 | No single quotes around placeholder paths that would break GitHub markdown | — verify |
After all 7 verified clean, copy everything between the two --- markers below into the issue body field at github.com/anthropics/claude-code/issues/new.
---
Summary
Claude Code's Write and Edit tools fail when the target path is inside a Windows directory protected by Controlled Folder Access (CFA) — Microsoft Defender's anti-ransomware feature. The same underlying kernel-level rejection surfaces as two distinct error codes depending on which file-system syscall is in flight:
Edit(modify existing) returnsEBADF: bad file descriptor, writeWrite(create new) returnsENOENT: no such file or directory, open
Both errors are misleading: the file descriptor is valid; the path exists; the kernel filter driver is blocking the syscall before the file-system layer sees it. The model receives no information about CFA being involved and burns retry budget on paths that are structurally fine but procedurally blocked.
This is one bug with two surface symptoms. Fixing only one error code leaves the other producing the same misleading-message class. Anthropic triage that catches both code paths gets a unified fix.
Environment
- OS: Windows 11 Home 10.0.26200
- Harness: Claude Code, VSCode native extension build, version
2.1.120(latest at time of filing; reproducible across multiple sessions on prior versions) - Models: reproduced on Opus 4.7 (1M context) and additional Claude models
- Shell: Git Bash (MSYS2) on Windows; PowerShell 7.x available
- Defender: Controlled Folder Access enabled with multiple user directories added to protected paths
- Allowlist state:
bash.exeandpwsh.exeare on the CFA executable allowlist; the Claude Code harness binary (bundled Node.js) is NOT
Verbatim errors returned to the model
For Edit on existing protected file:
EBADF: bad file descriptor, write
For Write (create new) on protected directory:
ENOENT: no such file or directory, open
In both cases that string is the entire error payload returned to the model — no stack trace, no errno cite beyond the macro name, no path echoed, no indication that CFA is involved.
Steps to reproduce
- On Windows 11, enable Microsoft Defender Controlled Folder Access (
Windows Security → Virus & threat protection → Ransomware protection → Manage Controlled folder access → On). - Add a directory to the protected list (e.g.
C:\protected-test\). - Verify allowlist state:
bash.exeandpwsh.exeon the CFA executable allowlist (typical default after first invocation); the Claude Code harness binary NOT on the allowlist. - Open a Claude Code session at any cwd.
- Place an existing file at
C:\protected-test\sample.ps1(any reasonable text content).
Reproducer A — Edit returns EBADF:
Ask the model to use the Edit tool to modify C:\protected-test\sample.ps1 (any non-trivial change).
| Step | Result |
|---|---|
| Read the protected file | SUCCEEDS |
| Edit with multi-line old_string/new_string | FAILS with EBADF: bad file descriptor, write |
| Edit retry, identical params | FAILS EBADF (same) |
| Read again to refresh harness FD | SUCCEEDS |
| Edit again with same params | FAILS EBADF (same) |
| Edit with a trivial diff (single space added) | FAILS EBADF (rules out content size) |
| Same Edit tool on a non-CFA path in the same session | SUCCEEDS |
Reproducer B — Write returns ENOENT:
Ask the model to use the Write tool to create a new file at C:\protected-test\new-file.tmp.
| Step | Result |
|---|---|
| ls C:\protected-test\ via Bash | SUCCEEDS (directory exists, listable) |
| Write new file inside the protected directory | FAILS with ENOENT: no such file or directory, open |
| Write retry, identical params | FAILS ENOENT |
| Same Write tool on a non-CFA path | SUCCEEDS |
Probe matrix (the load-bearing diagnostic)
The probe matrix isolates the blocked actor by running the same write attempt through different processes:
| # | Mechanism | Path class | Result |
|---|-----------|------------|--------|
| 1 | Edit tool, harness | CFA-protected, existing file (modify) | FAIL EBADF: bad file descriptor, write |
| 2 | Edit tool, harness | NON-CFA path | SUCCESS (boundary control) |
| 3 | Write tool, harness | CFA-protected, new file (create) | FAIL ENOENT: no such file or directory, open |
| 4 | Bash shell redirection (e.g. echo > path) via bash.exe | CFA-protected, new file | SUCCESS |
| 5 | Bash → powershell.exe -Command "Set-Content -Path <path>" | CFA-protected, new file | SUCCESS |
| 6 | Bash → python read+replace+write loop via python.exe (typical Windows install) | CFA-protected, existing file | SUCCESS |
Bash and PowerShell both succeed because both are on the CFA executable allowlist (typical default after first invocation, or admin-added). Python typically also lands on the allowlist if the user runs it from any CFA-protected path before. The Claude Code harness's bundled Node.js process is the single common element across the failures — it is not on the allowlist, so its WriteFile syscalls into protected paths are filter-driver-rejected.
Single common element identified, single fix surface.
Cross-operator reproducibility
Reproduced independently across multiple operators on the same Windows 11 host within a single week of investigation. Path-in-additional-working-dirs status does NOT predict outcome:
| Operator | Tool | Symptom | Path-in-additional-working-dirs |
|---|---|---|---|
| A | Write (~10 calls, control) | success on non-CFA paths | YES |
| A | Write + Edit, multiple, on CFA-protected paths | ENOENT + EBADF | YES (own deep workspace, but CFA-protected) |
| B | Write | ENOENT | NO (sibling-of-cwd, CFA-protected) |
| C | Edit | EBADF | NO (sibling-of-cwd, CFA-protected) — same path class as B with Edit instead of Write |
| D | Write (5+ times) | ENOENT | YES (own deep workspace, CFA-protected) |
Path-in-additional-working-dirs being YES does not prevent the failure. CFA-protected status is the predictor; AWD scope is not.
Boundary evidence (rules out other causes)
- The same harness, same session, same model, same
Edit/Writetool succeeds on a non-CFA path immediately before and after the failing call. Confirms it is not a generic tool fault, not per-session FD exhaustion, not a model/prompt issue. - File ACLs are normal
-rw-r--r--;Readworks on the protected file fine. Confirms it is not a Windows ACL permission issue. - The boundary is precisely the CFA scope. Files inside protected directories fail; files outside succeed.
- The failure does NOT depend on the harness's
additionalDirectoriesconfiguration (operators with AWD covering the path AND operators with AWD not covering the path both reproduce the failure).
Why CFA surfaces as EBADF (Edit) vs ENOENT (Write)
Controlled Folder Access enforcement runs in the Microsoft Defender kernel filter driver (MpFilter.sys). When a non-allowlisted process invokes a file-system syscall that targets a path inside a protected directory, the driver fails the syscall before the file-system layer sees it. Node.js (which the Claude Code harness uses) maps the underlying NTSTATUS rejection to libuv error codes — and libuv distinguishes between syscall paths:
Editperformsopen(...)(succeeds, FD valid) followed bywrite(fd, ...). The write is the syscall the filter driver rejects. libuv surfaces this asUV_EBADF, which lands in the JavaScript layer asEBADF: bad file descriptor, write. The FD itself is valid; the write is what's blocked.Write(creating a new file) performsopen(..., O_CREAT). The filter driver rejects the create attempt before the file ever exists. libuv surfaces this asUV_ENOENT, which lands asENOENT: no such file or directory, open. The path's parent directory exists and is listable, but the create-on-write is rejected at the kernel layer.
Same root rejection, two different libuv error mappings depending on whether the syscall is open-on-FD vs write-on-FD. Anyone fixing this on Anthropic's side will see two surface bugs from one underlying cause — catching both error codes in the tool layer is the unified fix.
Operational impact
- Each EBADF/ENOENT failure costs ~30 seconds of model tokens spent on retries against the same procedurally-blocked path, plus user-visible noise ("why is the agent retrying").
- The workaround is not discoverable from the error message alone; it requires the user (or the agent) to know about CFA and the executable-allowlist concept independently. Whichever agent first hits this on a given host has to discover the workaround the hard way; subsequent agents on the same host re-discover it independently.
- Workaround discipline became durable runbook material across multiple agents over a week of independent encounters. Tribal knowledge tax across an agent ecosystem.
- Allowlist-via-Defender-UI requires admin privilege (UAC). Allowlist contents are also admin-gated for enumeration — agents cannot probe in advance whether the harness binary is allowlisted; the only signal is the failure itself.
Workarounds that succeed
For text-class edits on existing files:
Bashheredoc: write a small PowerShell or Python script outside the CFA scope (e.g. into%TEMP%).Bashinvokespowershell.exe -NoProfile -ExecutionPolicy Bypass -File <script>(orpython <script>).- The external script reads the protected file, performs the edit in-memory, writes back. Because the executable is on the CFA allowlist, the kernel filter driver permits the write.
Readthe file post-write — shows the expected diff.
For new files in protected directories:
Writethe file to%TEMP%(non-CFA-protected by default).Bashcpfrom%TEMP%into the protected directory. Bash is on the CFA allowlist; the copy succeeds.- Optionally
rmthe%TEMP%source file via Bash for cleanup.
Both workarounds are reliable across multiple operators on the same configuration. Neither is discoverable from the harness's error messages alone.
Suggested fixes
Three options, ordered by user-friendliness:
- Catch CFA-class failures at the tool level and surface a clearer error to the model, distinguishing both code paths under one diagnostic message:
> Write blocked by Windows Controlled Folder Access. The Claude Code harness executable is not on the CFA allowlist for path "<path>". This affects both new-file creation (returns ENOENT) and existing-file modification (returns EBADF). Suggested: ask user to add the harness to CFA allowlist via Windows Security → Virus & threat protection → Manage ransomware protection → Allow an app through Controlled folder access; OR work outside protected paths; OR use Bash to invoke an allowlisted executable (powershell.exe, bash.exe, python.exe) to perform the write.
The current EBADF and ENOENT error strings are wrong on their face for this failure mode and give the model nothing to act on.
- Document this in the
WriteandEdittool descriptions so models can preemptively route through a CFA-allowlisted executable when working in protected paths. A single sentence in each tool's description would close the discoverability gap and avoid wasted retry cycles. Cite both error codes (EBADF, ENOENT) so models recognize either as the same class.
- Detect protected directories at session start or tool-call time by probing a write to a tempfile within the cwd's parent tree; classify directories as CFA-protected if
EBADF/ENOENTreturns; warn the model in the system context. This is heavier implementation but eliminates the wrong-path retry waste entirely. Could also surface a one-time toast/notification suggesting the user add the harness to the CFA allowlist.
A complementary option (out of scope for the tool layer but worth surfacing): add the Claude Code harness binary to a default CFA allowlist on installation. This is an installer-time decision that would eliminate the failure mode for new users without requiring them to learn about CFA. Existing installations would still benefit from options 1-3.
Additional context
- This is not a security-critical bug — CFA is doing exactly what it should (blocking writes from non-allowlisted processes to protected paths). The issue is the harness's surfacing of the failure to the model.
- Multiple operators independently hit this on the same machine within a week of each other (separate sessions, separate cwds, same machine, same Defender CFA configuration). Cross-agent reproducibility is high.
- Workaround via Bash + allowlisted shell is well-known internally now and saved as durable runbook material; it is tribal knowledge each new agent must independently discover.
- The probe matrix (Bash + PowerShell allowlisted, harness Node not) is empirically reproducible by any operator — a useful diagnostic for triage to confirm the mechanism on a Windows host with CFA enabled.
Thank you for Claude Code — the tooling has been valuable for our workflow; this specific error-class is the only surface where the harness's error reporting actively misleads the model.
---
After submission
Once the issue is filed, please share the issue number / URL back. I'll capture proof-of-submission to OB (paired with the Class A #53734 loop-close) and then route the Class C extract (which is a comment on existing issue #50289, with a placeholder for this Class B issue number that I'll fill in on submission).
What Should Happen?
When the Write or Edit tool fails because Windows Controlled Folder Access is blocking it, the harness should return a clear error message saying so — something like "Write blocked by Windows Controlled Folder Access; the Claude Code harness is not on the CFA allowlist for this path." The current EBADF and ENOENT errors are misleading because they make it look like there's something wrong with the file or its descriptor, when really Defender is blocking the write at the kernel level. A clear error would let the model either ask the user to add Claude Code to the CFA allowlist, work outside protected paths, or use Bash to invoke an already-allowlisted process (powershell.exe, bash.exe) to perform the write.
Both error codes (EBADF on Edit, ENOENT on Write) should surface the same diagnostic — they're two faces of the same underlying CFA rejection.
Error Messages/Logs
Two distinct error messages from the same underlying cause:
When the Edit tool tries to modify an existing file in a CFA-protected directory:
EBADF: bad file descriptor, write
When the Write tool tries to create a new file in a CFA-protected directory:
ENOENT: no such file or directory, open
That string is the entire error payload returned to the model — no stack trace, no errno cite beyond the macro name, no path echoed, no indication that Controlled Folder Access is involved.
Defender's filter driver event log (Event Viewer → Applications and Services Logs → Microsoft → Windows → Windows Defender → Operational, Event ID 1123) records the block on the Defender side at the same time, with the harness Node binary as the rejected process and the protected path as the target. That event is not visible to the harness or the model.
Steps to Reproduce
- On Windows 11, enable Microsoft Defender Controlled Folder Access:
Windows Security → Virus & threat protection → Ransomware protection
→ Manage Controlled folder access → toggle ON
- Add a directory to the protected list (e.g. C:\protected-test\).
- Confirm allowlist state: bash.exe and pwsh.exe are typically on the
CFA executable allowlist after first invocation; the Claude Code
harness binary (bundled Node.js) is NOT on the allowlist by default.
- Open a Claude Code session at any working directory.
- Place an existing file at C:\protected-test\sample.txt with any content.
REPRODUCER A — Edit returns EBADF:
- Ask the model to use the Edit tool to modify C:\protected-test\sample.txt
(any non-trivial change).
- Edit fails with: EBADF: bad file descriptor, write
- Read on the same file SUCCEEDS.
- Edit on a non-CFA-protected path in the same session SUCCEEDS.
REPRODUCER B — Write returns ENOENT:
- Ask the model to use the Write tool to create a new file at
C:\protected-test\new-file.txt.
- Write fails with: ENOENT: no such file or directory, open
- ls C:\protected-test\ via Bash SUCCEEDS (directory exists, listable).
- Write on a non-CFA-protected path in the same session SUCCEEDS.
PROBE MATRIX (the diagnostic that isolates the cause):
Run the same write attempt through different processes:
- Edit/Write tool (harness Node binary): FAIL
- Bash shell redirection via bash.exe: SUCCESS
- powershell.exe -Command "Set-Content -Path ...": SUCCESS
- python.exe read+replace+write loop: SUCCESS
Bash, PowerShell, and Python succeed because they are on the CFA
allowlist. The Claude Code harness's bundled Node.js process is the
single common element across the failures — it is not on the allowlist,
so its WriteFile syscalls into protected paths are filter-driver-rejected.
Single common element identified, single fix surface.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
Claude Code 2.1.120 (VSCode native extension build) on Windows 11 Home 10.0.26200
Platform
Other
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
WHY THE TWO ERROR CODES SHOW UP FOR THE SAME ROOT CAUSE
Defender's CFA enforcement runs in the kernel filter driver (MpFilter.sys).
When a non-allowlisted process tries to write to a protected path, the driver
fails the syscall before the filesystem layer ever sees it. Node.js (which the
Claude Code harness uses) maps the underlying NTSTATUS rejection to libuv error
codes — and libuv distinguishes between syscall paths:
- Edit performs open() (succeeds, FD valid) followed by write(fd, ...). The
WRITE is what the filter driver rejects. libuv surfaces this as UV_EBADF,
which lands in JavaScript as "EBADF: bad file descriptor, write". The FD is
actually valid; the write is what's blocked.
- Write (creating a new file) performs open(..., O_CREAT). The filter driver
rejects the create attempt before the file ever exists. libuv surfaces this
as UV_ENOENT, which lands as "ENOENT: no such file or directory, open". The
parent directory exists and is listable, but the create is rejected at the
kernel layer.
Same root rejection, two different libuv error mappings depending on whether
the syscall is open-on-FD vs write-on-FD. Catching both error codes in the
tool layer is the unified fix.
CROSS-OPERATOR REPRODUCIBILITY
Reproduced independently across multiple operators on the same Windows 11 host
within a single week of investigation. Path-in-additional-working-dirs status
does NOT predict outcome — operators with the path inside their AWD AND
operators with the path outside their AWD both reproduce the failure. CFA-
protected status is the predictor; AWD scope is not.
WORKAROUNDS THAT SUCCEED
For text-class edits on existing files:
- Bash heredoc: write a small PowerShell or Python script outside the CFA
scope (e.g. into %TEMP%).
- Bash invokes powershell.exe or python.exe to read the protected file,
perform the edit in-memory, and write back. Because the executable is on
the CFA allowlist, the kernel filter driver permits the write.
For new files in protected directories:
- Write the file to %TEMP% (non-CFA-protected by default).
- Bash cp from %TEMP% into the protected directory.
Both workarounds are reliable but not discoverable from the harness's error
messages alone — every new agent that hits this re-discovers the workaround
the hard way.
SUGGESTED FIXES (ordered by user-friendliness)
- Catch CFA-class failures at the tool level and surface a clearer error,
distinguishing both EBADF and ENOENT under one diagnostic message:
"Write blocked by Windows Controlled Folder Access. The Claude Code
harness executable is not on the CFA allowlist for path <path>. This
affects both new-file creation (returns ENOENT) and existing-file
modification (returns EBADF). Suggested: ask user to add the harness
to the CFA allowlist, OR work outside protected paths, OR use Bash
to invoke an allowlisted executable (powershell.exe, bash.exe,
python.exe) to perform the write."
- Document this in the Write and Edit tool descriptions so models can
preemptively route through a CFA-allowlisted executable when working
in protected paths. A single sentence in each tool's description would
close the discoverability gap.
- (Heavier) Detect protected directories at session start by probing a
write to a tempfile in the cwd's parent tree; classify directories as
CFA-protected if EBADF/ENOENT returns; warn the model in the system
context. Eliminates wrong-path retry waste entirely.
A complementary option: add the Claude Code harness binary to a default CFA
allowlist on installation. This is an installer-time decision that would
eliminate the failure mode for new users without requiring them to learn
about CFA. Existing installations would still benefit from options 1-3.
OPERATIONAL IMPACT
Each EBADF/ENOENT failure costs ~30 seconds of model tokens spent on retries
against the same procedurally-blocked path, plus user-visible noise. The
workaround is not discoverable from the error message alone; it requires
knowing about CFA and the executable-allowlist concept independently.
Allowlist-via-Defender-UI requires admin privilege (UAC), and allowlist
contents are also admin-gated for enumeration — agents cannot probe in
advance whether the harness binary is allowlisted; the only signal is the
failure itself.
This is not a security-critical bug — CFA is doing exactly what it should
(blocking writes from non-allowlisted processes to protected paths). The
issue is the harness's surfacing of the failure to the model.
Thank you for Claude Code — the tooling has been valuable for our workflow;
this specific error-class is the only surface where the harness's error
reporting actively misleads the model.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗