[BUG] Cowork on Windows: bash sandbox can create files on mounted host folder but unlink is denied — breaks all git write operations

Status Open
Reported on v2.1.121
Maintainer reply None cached
Activity 15 comments · opened May 1, 2026

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?

In Cowork on Windows, the bash sandbox can mount a selected host folder and perform read and create-write operations against it, but the unlink syscall is denied at the FUSE/mount layer. POSIX permissions and chmod have no effect — unlink is rejected regardless. sudo is not available (no-new-privs is set on the sandbox).
This is distinct from the reported stale-index.lock issue (#28546), which attributes the symptom to Windows file-handle latency. The behavior here is structural: a brand-new file created microseconds before the unlink attempt, on a path the sandbox just wrote, still fails to unlink. There is no race window where it works.
The practical effect is that all git write operations from the bash sandbox on a mounted Windows folder fail. git add partially succeeds but emits "unable to unlink" warnings on temp objects and .git/index.lock. git commit then fails because the prior .git/index.lock cannot be removed. Subsequent rm -f .git/index.lock from the bash sandbox also fails. Recovery requires the user to open PowerShell and clean up .git/ manually.
This makes the documented "Cowork can manage version control" workflow non-functional on Windows when the repo lives on a mounted host folder.

What Should Happen?

The bash sandbox should be able to perform unlink on paths under the mounted host folder, given that it can already create and write files there. Without unlink, git operations and any other workflow that uses temp/lock files cannot complete inside the sandbox.
If the unlink restriction is a deliberate security measure (preventing the sandbox from deleting host files), it should be documented, and the project-level workflow guidance should be updated to clarify that Cowork on Windows cannot perform git write operations on a mounted host folder.

Error Messages/Logs

$ git commit -m "test"
fatal: Unable to create '/sessions/<session>/mnt/<folder>/.git/index.lock': File exists.

Another git process seems to be running in this repository, e.g.
an editor opened by 'git commit'. ...

$ rm -f .git/index.lock
rm: cannot remove '.git/index.lock': Operation not permitted

Control test demonstrating the structural unlink denial:
$ cd /tmp && touch foo && rm foo && echo "tmp ok"
tmp ok

$ cd /sessions/<session>/mnt/<folder> && touch foo && rm foo
rm: cannot remove 'foo': Operation not permitted

Same file, freshly created microseconds before the unlink. POSIX permissions and chmod have no effect.

Steps to Reproduce

On Windows 11 (any recent build with WSL2 / Hyper-V Platform installed), open Cowork.
Select a folder that is a git repo (any folder under C:\Users\<you>\... works — does not need to be in OneDrive or Downloads, which have their own known mount issues).
Start a session and run the following control test in the bash tool:

uname -a
cd /tmp && touch foo && rm foo && echo "tmp ok"
cd /sessions/<session>/mnt/<folder> && touch foo && rm foo && echo "mnt ok"

The first rm succeeds. The second fails with rm: cannot remove 'foo': Operation not permitted.

Now reproduce the git failure:

cd /sessions/<session>/mnt/<folder>
echo "# test" >> .gitignore
git add .gitignore
git commit -m "test"

git add will emit unlink warnings; git commit will fail with Unable to create '.git/index.lock': File exists.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.121 (Claude Code)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

This is a Cowork-on-Windows bug. The form fields lean toward the Claude Code CLI but the Cowork sandbox runs Claude Code under the hood, so the same repo is the right place — see prior Cowork-tagged issues like #28546, #25096, #25293, #41710, #42520, #45433, #50873.

Related-but-distinct issues searched and ruled out as duplicates:

  • #28546 (stale .git/index.lock on Windows) — closest match, but reporter attributes the cause to Windows file-handle latency. The control test in this report (touch foo && rm foo on a brand-new file in the mount) rules that out: there's no race window, the unlink is structurally denied.
  • #11005 (closed NOT_PLANNED) — Linux/macOS scope, different platform.
  • #41710, #42520, #45433, #50873 — FUSE cache coherency / stale content issues. Different symptom (read returns wrong data); this report is a write-side issue (unlink denied).
  • #25xxx series — virtiofs/Plan9 mount-fails-to-establish issues. The mount in this report establishes successfully; the issue is what's allowed once it's mounted.

The repo I tested against is on local disk under the user profile (NOT OneDrive, NOT Downloads), so the special-folder mount issues don't apply.

Repro environment: Windows 11, WSL2 newly installed (Cowork's bash sandbox booted successfully on first session after the install), Cowork session in a fresh chat, no other modifications.

View original on GitHub ↗

15 Comments

github-actions[bot] · 5 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/53396

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

trevor127001 · 5 months ago
Found 1 possible duplicate issue: 1. Cowork VM SDK install on Windows ARM64: hundreds of Operation not permitted errors during node_modules cleanup #53396 This issue will be automatically closed as a duplicate in 3 days. If your issue is a duplicate, please close it and 👍 the existing issue instead To prevent auto-closure, add a comment or 👎 this comment 🤖 Generated with Claude Code

Not a duplicate of #53396. Both involve "Operation not permitted" on rm, but they're separate bugs:

  • #53396 fails on Cowork's own internal install path (claude-code-vm/<version>/node_modules/) on Windows ARM64. The reporter attributes it to NTFS file-handle semantics — another process holding open handles without FILE_SHARE_DELETE.
  • This issue fails on the user-selected mounted host folder on Windows x86_64. The control test (touch foo && rm foo on a freshly-created file in /tmp succeeds; same test on the mount fails) rules out the file-handle theory: no other process has ever opened the file. The denial is structural at the FUSE/mount-policy layer, not racy.

The user-facing impact is also different: #53396 is log spam during SDK install, this is a complete block on git write workflows in mounted user folders. Different path, different cause, different mitigation.

rdnfn · 5 months ago

I appear to have the same problem via cowork on macos, with failing unlinking in the sandbox appearing to be the root cause of the issue. Thanks for fixing this!

Claude-Cowork-generated summary of the issue on my side (Desktop app version: (1.5354.0 (9a9e3d) 2026-04-29T01:14:34.000Z):

In Cowork (Claude Code 2.1.121, macOS, model claude-opus-4-7), every git invocation leaves orphan files in .git/ that the sandbox cannot unlink. After one commit I see HEAD.lock, next-index-N.lock, index.lock, maintenance.lock, and tmp_obj_ files, and the next git command then fails with "Another git process seems to be running" until I manually rm the locks from a real terminal. GIT_OPTIONAL_LOCKS=0 and --no-optional-locks only address the optional index-refresh path; they do not prevent HEAD.lock, next-index-.lock, or tmp_obj_* orphans, since those are created by normal commit operations, not opportunistic ones. Root cause looks like the sandbox mount allowing create but denying unlink inside .git/. Combining git add and git commit into one invocation reduces the count but does not eliminate it.
leoravagnani · 5 months ago

Seeing the very same issue described above. Git leaves orphan files when using Cowork on macOS.

Snow333 · 5 months ago

Independently hit this as well (Windows 11, local disk). Confirming the structural unlink denial. Cowork bash could not remove .git/index.lock no matter how much we huffed and puffed to blow this house down. Ended up using PowerShell locally to clean up index.lock between every Cowork to git mediated operation. Every. Operation. No keyboards were destroyed in the repeated attempts to explore workarounds.

When using Claude Code (Desktop and CLI) I did not hit this issue. Bummer, as I'm enjoying Cowork Desktop and am excited to see this resolved.

I have the diagnostic data Claude captured/created during this. Eg. file size deltas vs HEAD, SHA256 hashes, session timing, totally down to share all 10KB of this data if needed.

trevor127001 · 5 months ago

Another workaround pattern, complementary to @Snow333 's: rather than cleaning up index.lock after the fact, we route all git writes through a PowerShell wrapper on the host so the sandbox's unlink denial never trips. Works reliably across commit / branch / stash operations. Script available if helpful.

cosmosmaxis · 5 months ago

I have the exact same issue

tisalvadores · 5 months ago

TL;DR

The root cause of this issue is that Cowork's bindfs mount is configured with the --delete-deny option, which is bindfs's documented policy for making unlink() and rmdir() return EPERM on the mount, resulting in a denied unlink.

Removing that flag fixes this issue, but --delete-deny is probably a deliberate anti-data-loss safeguard (preventing the sandbox from deleting host files), so the safety implications of removing it would need to be reviewed. A narrower allowlist could meet the same intent without breaking git and standard atomic-write workflows. The permission-asymmetry test below isolates the configuration exactly. Detailed evidence and proposed fix follow.

---

_Claude Assisted Bug Report_

Reproducing this on a separate Windows install. We have direct evidence Cowork's mount is bindfs with --delete-deny enabled, which is the root cause of this issue. While testing, we also reproduced #42520's read-side cache pathology on the same bindfs mount in the same session — independent bug, independent fix, but co-located in the same Cowork mount-setup code.

Environment: Cowork in the Claude desktop app on Windows. The bash sandbox reports claude --version = 2.1.138 (Claude Code) (newer than #55206's 2.1.121 — bug persists through CC updates). Sandbox is Ubuntu 22.04.5 LTS, kernel 6.8.0-106-generic. bindfs 1.14.7 (current stable, no upstream bugs in this area — this is a Cowork configuration issue).

The smoking gun: permission asymmetry matches bindfs --delete-deny exactly

bindfs's documented --delete-deny option disallows file deletion while leaving all other operations untouched. We ran every destructive syscall on a probe directory in our mounted workspace folder:

| Operation | Result | bindfs flag implicated |
|---|---|---|
| mkdir, touch, write, chmod, truncate, utimes, link, symlink | ✅ all succeed | — |
| mv (rename) | ✅ succeeds | --rename-deny OFF |
| unlink | ❌ EPERM | --delete-deny ON |
| rmdir | ❌ EPERM | --delete-deny ON |

strace confirmed the failure is unlinkat(...) = -1 EPERM at the kernel syscall level, not a higher-level interpretation. Python reports errno=1 (EPERM). Read-only mounts return EROFS instead of EPERM, separating the RO-mount semantics from the --delete-deny policy.

Direct evidence bindfs is in use

  • bindfs 1.14.7 is installed at /usr/bin/bindfs in the sandbox. (Why would it be there if it weren't being used?)
  • Mount-source path from /proc/self/mountinfo shows /mnt/.virtiofs-root/shared/c/Users/<user>/<folder> as the source of the FUSE mount at /sessions/<id>/mnt/<folder> — exactly the NTFS → virtiofs (host↔VM) → bindfs (FUSE) → sandbox stack #42520 reverse-engineered.
  • One FUSE connection per mount in /sys/fs/fuse/connections/ (five connections for five mounts) — bindfs's signature, not virtiofsd's.
  • Mount options match bindfs defaults: user_id=0,group_id=0,default_permissions,allow_other.
  • chmod is force-filtered to 0700 regardless of the requested mode (chmod 777 → 0700, chmod 644 → 0700, etc.; same chmod on tmpfs from the same process works normally). bindfs has a --perms option that does exactly this. Confirms the bindfs layer is actively rewriting metadata, not passively forwarding.

Direct reproduction of #42520's read-side cache pathology in the same session

Same mount, same shell session. Coordinated test with PowerShell on the host:

  1. Sandbox wrote 100 'A' bytes to _cache_test.txt. Sandbox view: size=100, sha256=d82c6aa1.... Correct.
  2. From PowerShell on the host, replaced the file with 16-byte "host_wrote_short". Host-side Get-Item.Length=16, Get-FileHash=f434f899.... File is genuinely 16 bytes on NTFS.
  3. Sandbox reads of the same file: size=100 (the old sandbox-originated size), sha256=d3853c93... (= 16 host bytes + 84 null bytes). This is exactly the pathology #42520 documents: FUSE size cache stuck at the last sandbox-originated value, host content visible up to that size with null-padding beyond.
  4. Cache TTL probe: re-read at +5s, +15s, +89s, +111s after the host write — every read returned the same stale size=100, sha256=d3853c93.... No natural cache expiration observed within 111 seconds.
  5. Manual flush attempts that did NOT invalidate: ls -la <file>, fresh open()+read(), ls -la of parent dir, sandbox-side touch (utimensat). The cache is opaque to userspace invalidation hints from the sandbox side.
  6. Sandbox writes invalidate asymmetrically: open('wb') (truncate-and-write) correctly updates the cached size; open('r+b').write() (partial overwrite without truncate) does not update the cached size even after the bytes change.
  7. Write-through still works correctly: after a sandbox open('wb').write(b'sandbox_overwrite'), host-side Get-FileHash confirmed the file on NTFS matched the sandbox's last write exactly. So the bug is purely on the read-side invalidation path — writes propagate fine.

Connection to the read-side issue cluster

The write-side issues (this report and #55206 itself) and the read-side cache-coherency cluster (#38993, #41710, #42520, #45433, #50873) are independent bugs with independent root causes — but they are co-located: both surface from Cowork's bindfs mount on Windows, reproducible simultaneously on the same mount instance. They likely land on the same engineering team and could plausibly share a single triage thread, but two distinct fixes are needed:

  • --delete-deny is ON, causing the write-side EPERM symptoms in this issue and #55206. Fix is well-defined (see below).
  • Cache invalidation is broken on the same bindfs layer, causing the read-side symptoms in #38993 / #41710 / #42520 / #45433 / #50873 (stale content, null-padding, host writes invisible, divergent answers between Cowork and Claude Code tabs). Exact root cause not nailed down by this report — see "Suggested fix" below.

Persistence

EPERM behavior survives: Cowork app restart via Task Manager, "Workspace needs to be reinstalled" prompt, full Windows reboot, fresh Cowork session pointed at a brand-new empty folder created moments earlier in PowerShell, and two distinct sandbox user identities (intelligent-compassionate-wright, gallant-sweet-babbage) across separate sessions. Not transient state, not per-folder, not per-user.

Workaround

For git: plumbing-path commit pipeline avoiding .git/index.lock by working on an alternate index in tmpfs (which is exempt from EPERM):

cp .git/index /tmp/myindex
GIT_INDEX_FILE=/tmp/myindex git update-index --add <paths>
TREE=$(GIT_INDEX_FILE=/tmp/myindex git write-tree)
COMMIT=$(echo "<msg>" | git commit-tree $TREE -p HEAD)
echo "$COMMIT" > .git/refs/heads/main   # bypasses HEAD.lock
cp /tmp/myindex .git/index              # bypasses index.lock

Produces structurally correct commits. Stale .git/index.lock, .git/HEAD.lock, and orphan tmp_obj_* blobs accumulate (cleanup via PowerShell Remove-Item + git gc --prune=now). The fact that tmpfs is exempt from EPERM is itself evidence the issue is at the bindfs layer, not virtiofs or anything below.

Suggested fix

Remove --delete-deny from Cowork's bindfs invocation. That single change resolves this issue and #55206. If --delete-deny was added deliberately as an anti-data-loss safeguard ("don't let the sandbox delete user files"), the same intent could be met by a narrower allowlist that permits .git/ lock files and tempfile+rename+unlink atomic-write patterns. Either way, removing the blanket deny is necessary for git and most standard write workflows to function inside the sandbox.

The read-side cluster requires a separate fix that this report doesn't fully characterize. The exact root cause could be excessive FUSE attr_timeout / entry_timeout settings, missing invalidation propagation from virtiofs through bindfs to the kernel, bindfs itself ignoring or mis-handling invalidation events, or a deeper architectural issue with bindfs-over-virtiofs cache coherency when the underlying file is written from outside the bindfs instance. The observed asymmetric invalidation on sandbox writes (truncate+write does invalidate, partial overwrite does not) is a clue that may rule out "just tune the timeouts" as a complete fix. Reverse-engineering the exact mechanism requires access to Cowork-internal mount-setup code.

bjcoombs · 4 months ago

Confirming on macOS (Darwin 24.2.0, current Claude desktop) — the bindfs --delete-deny root cause @tisalvadores identified isn't Windows-specific; the same mount policy is active on the macOS sandbox.

Reproduction:

$ touch .probe && rm .probe && echo "WRITE OK"
rm: cannot remove '.probe': Operation not permitted

Folder is a plain local path under /Users/<u>/dev/..., not iCloud-synced, no file provider, no chflags, Full Disk Access granted to Claude. Cowork's bash sandbox can touch the file but not rm it.

Cascading git failures observed: git add → fatal: Unable to write new index file; git pull → warning: unable to unlink '.git/objects/maintenance.lock'; git push rejected non-fast-forward because fetch couldn't update refs. Cowork's own Read/Write/Edit tools succeed (they don't go via the bash mount path).

Persistence ruled out on macOS across: Claude desktop quit + restart, full Mac reboot, removing + re-granting Full Disk Access for Claude, and a fresh Cowork session pointed at the same folder (different probe filename, identical EPERM). Structural to the mount, not session/state.

Regression marker: committing and pushing from Cowork on this folder used to work for me. So either --delete-deny was added in a recent Cowork update, or some other change made the same flag start tripping where it previously didn't. Worth a look at when in the release history this flag was introduced or changed on the macOS build.

Workaround: commit/push from a native shell (Claude Code CLI on the same path works fine — same files, different I/O route, no bindfs in the way). Work stays on disk; only GitHub backup trails the Cowork session until then.

yurukusa · 4 months ago

Adding this thread to the broader Cowork sandbox failure-surface cluster framing I've been tracking, plus a CLI-side partial defense.

This is structurally the same shape as #38993 (virtiofs FUSE serves stale files) and #62932 (P1 critical, FUSE inode metadata frozen for the session) — the FUSE bridge between the Windows host and the Linux sandbox honours some POSIX operations (read, create-write) but rejects others (unlink, content invalidation). The unlink-denial you describe sits next to the stale-content / NUL-padded shrink already documented in those threads. Five sub-patterns now articulated in the bridge layer:

  1. Stale content + stat after first read (#38993, #62932)
  2. Working-tree git walks return phantom data (#62932)
  3. Pre-existing files invisible through the mount outside home directory (#40973)
  4. NUL-padded shrink on .git/config after host-side merges (#62932)
  5. Unlink denied at the FUSE layer regardless of POSIX perms (this issue, #55206) — the case you've isolated to a structural rejection rather than a Windows-handle race

I shipped a CLI-side advisory hook today (2026-05-28) that partially covers the unlink-denial case: cowork-fuse-staleness-watcher.sh (PR #410, 25 tests passing). The matcher already fires on git rm / git clean / git mv inside /sessions/<id>/mnt/ (and on git add / commit whose internal unlinks against .git/index.lock are the actual failure path you describe), so a CLI session that runs git commit over the mounted Windows folder will see the warning before the partial git add lands and leaves a stuck .git/index.lock.

The hook does NOT fix the underlying FUSE rejection — that's server-side from the operator's perspective. What it does cover:

  • Surfaces the unlink-denial risk before the bash command runs, with the recovery path (PowerShell-side .git/ cleanup, which matches your repro)
  • Recommends Cowork's first-class file tools (Read / Edit / Write) which bypass FUSE entirely and are authoritative for the working tree
  • Recommends ref-only ops (git rev-parse / log / show) which don't touch the unlink path and remain functional

The five-sub-pattern framing of Cluster 11 is in cc-safe-setup/docs/cluster-tracker.html (free reference). Whether an Anthropic-side acknowledgment of unlink-denial as a structural-not-racy bug would surface in the changelog or the Cowork docs would be useful for operators triaging this case — happy to refine the framing if more sub-patterns come in.

rcc8814-star · 3 months ago

Confirmed: unlink from bash breaks all git operations on Windows mounts

We experienced this repeatedly on a Flutter project mounted on Windows via VirtioFS in Claude Cowork / Claude Desktop.

What happens: Git operations (commit, rebase, pull) leave .git/index.lock, .git/HEAD.lock, refs/heads/master.lock, and .git/objects/maintenance.lock behind. Any attempt to rm/unlink these from the Linux bash sandbox fails silently or with permission denied — even for files the sandbox just created. They're visible in ls, but rm -f returns exit 0 and the file remains.

Cascading effect: once a single lock file accumulates, all subsequent git commands fail with "unable to lock" errors, making git unusable from the sandbox for the rest of the session.

Reliable workaround: write a .bat file from the sandbox (writes work fine), then run it from Windows File Explorer — the native Windows process has the NTFS permissions the sandbox lacks, so del /f /q on the locks actually succeeds.

Alternative workaround: use git plumbing (GIT_INDEX_FILE + read-tree + write-tree + commit-tree + push by hash) which never creates the lock files in the first place.

Environment: Anthropic Cowork / Claude Desktop, Windows 11, VirtioFS mount, Git 2.34.1 (Linux sandbox).

bjcoombs · 3 months ago

Follow-up from the same macOS setup (Darwin 24.2.0), two things since my 23 May comment.

Why locks specifically get orphaned — a rename-vs-unlink asymmetry. Git's lockfile protocol finishes a successful save by renaming the lock into place (index.lock → index), which bindfs allows; it only calls unlink() on the lock when cleaning up an aborted/interrupted save, which --delete-deny blocks. So completed commits leave nothing, but any save killed mid-flight orphans a lock the sandbox can't remove — .git/index.lock / .git/HEAD.lock then accumulate and block the next session with "Another git process seems to be running." Confirmed today: both rm and mv on .git/index.lock return Operation not permitted inside .git/.

In-Cowork recovery (didn't have this on 23 May). You don't have to drop to a native shell. When a git write hits the orphaned lock, Cowork prompts to grant delete access to the working folder; allowing it lets rm .git/index.lock succeed from the bash sandbox and the save proceeds. Allowing it persistently means a future orphaned lock can be cleared in-session instead of blocking until you switch to the CLI.

Common real-world trigger. The single-rm repro above is the root cause; in practice the orphaned lock most often arises from concurrency. Running two Cowork chats on the same folder, both on an auto-"commit and push after every change" workflow, reliably collides on the git index/ref lock ("Another git process seems to be running"). On a normal filesystem that race is transient — the loser retries, the winner's lock clears. Here, because the sandbox can't unlink inside .git/, any collision that leaves a lock behind can't be cleared from within Cowork, so a normally self-healing race becomes a hard block. Two auto-saving chats on one folder reproduces it quickly. (The contention is deterministic; whether a given collision orphans a lock depends on a save being interrupted while holding it — which the concurrency makes likely.)

rcc8814-star · 3 months ago

Thanks for laying out the root cause so clearly — that rename/unlink
asymmetry explains exactly what I'd been seeing on my end (VirtioFS on
Windows).

One thing that might add to the picture: today, working in Cowork on
Windows, I ran into something related but a bit different. A native Windows
script wrote a file and confirmed on the spot, using a native command, that
the content landed
correctly on disk. But reading that same file from the Linux sandbox (cat,
wc -l) kept returning the old version — same byte size, same mtime as
before the change — for over a minute afterward. So the write itself was
fine; what failed was the sandbox never invalidating its cached view of the
file. It only cleared up once I opened the file in a native Windows app
(Notepad), which showed the updated content right away.

Might be the flip side of what you described: if the rename does propagate
but the sandbox doesn't refresh its read cache for files changed from
outside it, that
would explain cases where it looks like "the write never landed" when it
actually did — it's the read afterward that's lying.

Thanks again for digging into this,
Rober

El sáb, 20 jun 2026 03:44, Ben Coombs @.***> escribió:

bjcoombs left a comment (anthropics/claude-code#55206) <https://github.com/anthropics/claude-code/issues/55206#issuecomment-4756758438> Follow-up from the same macOS setup (Darwin 24.2.0), two things since my 23 May comment. Why locks specifically get orphaned — a rename-vs-unlink asymmetry. Git's lockfile protocol finishes a successful save by renaming the lock into place (index.lock → index), which bindfs allows; it only calls unlink() on the lock when cleaning up an aborted/interrupted save, which --delete-deny blocks. So completed commits leave nothing, but any save killed mid-flight orphans a lock the sandbox can't remove — .git/index.lock / .git/HEAD.lock then accumulate and block the next session with "Another git process seems to be running." Confirmed today: both rm and mv on .git/index.lock return Operation not permitted inside .git/. In-Cowork recovery (didn't have this on 23 May). You don't have to drop to a native shell. When a git write hits the orphaned lock, Cowork prompts to grant delete access to the working folder; allowing it lets rm .git/index.lock succeed from the bash sandbox and the save proceeds. Allowing it persistently means a future orphaned lock can be cleared in-session instead of blocking until you switch to the CLI. — Reply to this email directly, view it on GitHub <https://github.com/anthropics/claude-code/issues/55206?email_source=notifications&email_token=B4GSVDXPB7EXKMMXSMH2FIL5AYXDBA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINZVGY3TKOBUGM4KM4TFMFZW63VHMNXW23LFNZ2KKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#issuecomment-4756758438>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/B4GSVDX65E2AI3PNMLMLXNT5AYXDBAVCNFSNUABFKJSXA33TNF2G64TZHM4TGNZSGUZTINZVHNEXG43VMU5TIMZWGI3DOMJXG4Z2C5QC> . You are receiving this because you commented.Message ID: @.***>
peedawg · 3 months ago

Also reproduces on macOS — not Windows-specific.

Same structural unlink() denial on a Cowork session on macOS (Apple silicon). The repo is a normal local folder, mounted into the sandbox over FUSE (fuseblk / type fuse). POSIX perms show the files owned by the sandbox uid, but unlink is rejected regardless; rename() and in-place overwrite are allowed.

Control test (mount vs. off-mount, brand-new file, no race window):

off the mount (sandbox ext4) — fine

$ cd /tmp && touch foo && rm foo && echo ok
ok

on the mounted repo — denied

$ cd /sessions/<session>/mnt/<repo> && touch foo && rm foo
rm: cannot remove 'foo': Operation not permitted

The knock-on for git is worse than "commit fails": because git clears .git/index.lock with unlink(), even a read-only git status leaves a fresh index.lock behind that the sandbox then cannot remove, so it blocks the next command:

$ git status
warning: unable to unlink '/sessions/<session>/mnt/<repo>/.git/index.lock': Operation not permitted
$ find .git -name '*.lock'
.git/index.lock # ← left behind by a plain status, blocks the next op

So the friction is continuous, not occasional — each git invocation poisons the following one.

Filesystem-only workaround that does work (since rename is permitted but unlink is not): keep the working tree on the mount but move git's metadata off-mount, so all lock files land on a filesystem where unlink works:

git --git-dir=/tmp/repo.git --work-tree=/sessions/<session>/mnt/<repo> <cmd>

cp -a <mount>/.git /tmp/repo.git first (copy is create+read, both allowed). Commits/pushes then run clean with zero orphaned locks on the mount; the commit is delivered to origin and the host repo syncs by pulling. This is a palliative, not a fix — the underlying request stands: the sandbox should permit unlink() on the mounted folder (or the "Cowork can manage version control" workflow should be documented as unavailable on a mounted host repo until it does).

Suggested label update: this is not platform:windows only — please broaden to include macOS.

itsmackiecj · 1 month ago

Confirming this on Windows 11 as of 5 September 2026, with an important nuance: the wording of the most-common git failure message points the model and user at concurrency, not permissions, as the problem, so a model won't apply the obvious, easy fix unless the user knows to ask.

The obvious, easy fix: Cowork exposes a tool, allow_cowork_file_delete, whose own description says to call it "whenever a delete operation (such as rm) fails with 'Operation not permitted', rather than telling the user it is impossible." Invoking it and approving the prompt enables deletion for that folder. To confirm this (within one session):

# before
$ rm _to_delete/probe.txt
rm: cannot remove '_to_delete/probe.txt': Operation not permitted

# after approving allow_cowork_file_delete for the folder
$ rm _to_delete/probe.txt          # succeeds
$ rmdir dprobe                     # succeeds
$ python3 -c "import os; os.unlink('p2')"   # unlink(2) OK

With the grant in place, git is simply normal. A full commit cycle stranded zero lock files and zero tmp_obj_* orphans; git status stranded nothing; and git rm, git stash, git reset --hard, git gc --prune=now and branch switching all succeeded, git fsck --full --strict clean throughout.

The grant is folder-scoped and does not persist across sessions — separately tested: a fresh session hit Operation not permitted again, and the grant had to be re-made.

As a security design that seems right: explicit, scoped, ephemeral, nothing accumulating. But it makes the discoverability problem structural rather than one-time. The default is denied, every session, and the failure surfaces as fatal: Unable to create '.git/index.lock': File exists. Another git process seems to be running — which is misleading. It points at concurrency, not permissions, and names no remedy. Nothing in that message, nor in the rm: Operation not permitted that follows, suggests a permission grant exists.

Both this thread and #61343 concluded no in-session recovery was available and documented host-shell cleanup as the fix; #61343 asks outright whether "a sanctioned way to run git commit" exists. Because the grant is per-session, that wrong turn is taken again every session, by every user, indefinitely.

The remaining findings below were all measured in the default, delete-denied state, which is what every user hits first and what the model hits before it knows to ask. Two of them contradict what's currently in this thread, one is a silent data-integrity problem I don't think has been reported, and the last points at a
one-flag fix.

1. rename(2) succeeds, including over an existing destination

This contradicts #61343, which lists "mv over existing destination" among the failing operations. On the tested Windows mount it does not fail:

$ python3 -c "import os; os.rename('src.txt','dst.txt')"   # dst.txt already exists
# succeeds; dst.txt now holds src.txt's content, src.txt is gone

$ python3 -c "import os; os.unlink('u.txt')"
OSError: [Errno 1] Operation not permitted: 'u.txt'

So the mount denies unlink(2) specifically, not removal-of-data generally — rename-over-existing destroys the target inode just as effectively and is allowed. mkdir and directory rename also work; rmdir does not.

That asymmetry makes a complete in-sandbox workaround possible, which answers the question #61343 asks directly ("Is there a sanctioned way to run git commit such that lock-file cleanup succeeds? We haven't found one"). Move the locks aside instead of deleting them, into a folder inside .git/ so it never shows up as untracked:

D=$(git rev-parse --absolute-git-dir) && mkdir -p "$D/_to_delete" \
&& find "$D" -maxdepth 3 -name "*.lock" \
   -exec sh -c 'mv "$1" "$2/_to_delete/lock-$(basename "$1").$(date +%s%N)"' _ {} "$D" \; 2>/dev/null

Run that before every git command and commits work indefinitely without host-side intervention. Same trick for .git/objects/**/tmp_obj_* after each commit. I've run thousands of commits this way (before I found the permissions fix, this was my default workaround); git fsck --full --strict stayed clean throughout, reporting only the expected dangling objects. The quarantine folder must live inside .git/ and on the same filesystem — a cross-device mv falls back to copy-then-unlink and fails.

This doesn't reduce the severity of the bug, but it does mean the Windows workflow is recoverable in-session rather than "non-functional", and nobody needs to alt-tab to PowerShell mid-task.

2. The blocking lock is HEAD.lock, and git status is what strands index.lock

The original report says git add strands .git/index.lock and that this is what breaks the following git commit. That isn't the mechanism I measured. git add and git commit rename index.lock onto index, and rename works (§1), so no index lock survives them. What actually survives a commit is
HEAD.lock, and that's what kills the next one:

$ git commit -m "second"
warning: unable to unlink '.../.git/objects/6d/tmp_obj_UXt2ds': Operation not permitted
fatal: cannot lock ref 'HEAD': Unable to create '.../.git/HEAD.lock': File exists.

index.lock does strand, but from git status, which takes the lock to refresh the index and then rolls it back — a rollback is an unlink, so it fails. A read-only command poisons the next write. In testing, a bare git status was enough to make the following git stash abort.

Practical consequence for anyone scripting around this: the sweep has to run before every git invocation, git log and git status included, not just before writes.

Commit also strands .git/objects/maintenance.lock via auto-maintenance; -c gc.auto=0 -c maintenance.auto=false prevents that one entirely.

3. git gc fails and damages the repo on its way out

git gc is the natural thing to reach for as cleanup, but it makes things worse. It locks each ref to expire the reflog, fails to release the lock, then collides with its own stranded lock on the next ref:

$ git gc --prune=now
error: cannot lock ref 'refs/heads/master': Unable to create '.../refs/heads/master.lock': File exists.
fatal: failed to run reflog

It exits leaving refs/heads/master.lock in place, so the next git command also refuses to run until that lock is swept. Since gc --auto can fire from an ordinary commit once enough loose objects accumulate, this can be triggered without ever typing gc — which is a good argument for the sandbox setting gc.auto=0 by default on a mounted repo.

4. git checkout <branch> silently commits files onto the wrong branch

I found this in testing, not live (thankfully), because it's a bear: a successful operation does something other than what you expect, reports no errors, and sets you up for real pain down the road.

If the target branch lacks a file present in the current branch, git cannot delete that file from the working tree. It switches the branch anyway and leaves the file behind as untracked:

$ git checkout -b feature && touch feature_only.txt && git add -A && git commit -m "..."
$ git checkout master
$ git branch --show-current
master
$ git status --short
?? feature_only.txt        # a feature-branch file, sitting on master

The next routine git add -A commits feature_only.txt onto master without comment. In my session it also cascaded: the orphaned files later blocked a merge with "untracked working tree files would be overwritten."

Because Cowork commits autonomously, this is worse here than it would be in an interactive shell — the contaminating add -A is exactly what an agent does next, and I'm not reading git status in between.

The other working-tree deletions at least fail loudly:

| Operation | Result |
|---|---|
| git rm <file> | fatal: git rm: 'f': Operation not permitted |
| git reset --hard (when a file must go) | fatal: Could not reset index file to revision 'HEAD' |
| git stash | aborts |
| git checkout <branch> | succeeds with wrong results |

Until the mount permits unlink, the only safe pattern on a mounted repo is single-branch and append-only. Safe in practice: add, commit, log, diff, show, fsck, rm --cached.

5. The rename/unlink asymmetry looks like bindfs --delete-deny

As others have noted, the root cause of all this comes down to a simple mount-option change: bindfs exposes --delete-deny and --rename-deny as separate flags. The problem behavior, unlink and rmdir denied, rename permitted even over an existing destination, is --delete-deny set without --rename-deny.

Dropping --delete-deny for the connected folder resolves findings 1 through 4 at once, and the security posture barely changes: a sandbox that can already rename() over an existing file can
already destroy arbitrary content in the folder. --delete-deny on its own prevents very little while breaking git automation painfully.

A note on the plumbing workaround in this thread

The suggestion (by user rcc8814-star on 14 June) to use GIT_INDEX_FILE + write-tree + commit-tree and write the ref by hash does work, but there's a risk: "never creates the lock files in the first place" holds only for that exact sequence. git status and git log still strand index.lock on the mount, and write-tree/commit-tree still leave tmp_obj_* orphans at the same rate as a
normal commit. Two further cautions for anyone adopting it: git update-index --add <paths> stages additions only, so a file removed from the working tree stays in the commit silently (measured — the commit contained a file that was no longer on disk, with no warning); and writing the ref with echo > .git/refs/... skips the reflog entirely, so those commits never appear in git reflog. Using
git update-ref instead preserves the reflog at the cost of one HEAD.lock.

6. In practice the trigger is usually concurrency, not a lone rm

The single-rm control test above is the root cause, but it is not the only way people encounter the issue. Two Cowork chats open on the same folder, both committing after each change, collide on the index or ref lock quickly — "Another git process seems to be running." On a normal filesystem that race is transient: the loser retries, the winner's lock clears. Here, any collision that leaves a lock behind cannot be cleared from inside the sandbox, so a self-healing race becomes a hard block. Two auto-committing sessions on one folder reproduces it fast, and is a better repro than the control test for anyone trying to hit it naturally.

Suggestions, in priority order

  1. Surface the permission grant at the point of failure. This is the cheapest fix with the largest effect. When a git command fails on a stranded lock, or any unlink returns EPERM under /sessions/*/mnt/, say that delete permission can be granted and offer it. Today the error blames a phantom concurrent git process, which sends people to ps and to host-side cleanup rather than to the one-click remedy that already exists.
  2. Reconsider the default — --delete-deny without --rename-deny protects almost nothing. I initially assumed the denial at least preserved the inventory of a destroyed folder, on the reasoning that a truncated file still has a name and a directory entry. But rename(2) over an existing destination removes the source's directory entry, so renaming files onto one another is a deletion primitive. Starting from 56 files in 10 subdirectories, with unlink denied throughout, renames and truncations alone reduced the tree to a single file. rename(2) also replaces an existing empty directory (non-empty gives ENOTEMPTY), so directories collapse the same way once emptied. The floor is one file and one subdirectory, because the last rename needs a target. No inventory survives.

What the denial does still buy is protection against accidental deletion, since ordinary buggy code calls unlink() rather than renaming siblings onto each other. That is not worth breaking git by default, and it is certainly not worth producing a spurious error about a concurrent git process. If the isolation is the actual goal, --delete-deny needs --rename-deny beside it. That would break much more than git: I can only imagine it being done as part of a general reworking of how permissions are managed.

  1. Fix the checkout case regardless. Even as a transient pre-grant state, refusing the branch switch would be better than completing it incorrectly.
  2. Set gc.auto=0 and maintenance.auto=false on repos under the mount, so auto-maintenance can't trigger finding 3 unprompted.
  3. Document the constraint and the rename-based workaround, or build the sweep into the sandbox's git invocation. Right now, as far as I can see, every Windows user rediscovers this from scratch.

Environment: Windows 11, Cowork session, mount reports fuseblk, git 2.34.1
inside the sandbox. Happy to rerun any of these with strace attached.