[BUG] Edit reports success and reads back the edit, but never writes to disk (worktree + PreToolUse hook; no sandbox)
Summary
In a git worktree with a PreToolUse hook registered on Edit|Write|MultiEdit, the Edit tool reported success and read back the edited content, but the change never reached the filesystem. Every Bash-side observer (grep, git diff, node, python3, stat) saw the original file, including with dangerouslyDisableSandbox: true.
This is closely related to a cluster of issues closed as NOT_PLANNED — #40227, #35699, #52463 — but the trigger here is different: no sandbox is configured (#35699's trigger), the path is absolute and correct (#52463 / #60679's trigger), and writes to other files in the same session and same directory persisted fine.
The failure is silent and, without an independent verification step, produces confidently wrong work: an edit is "applied", the model proceeds, and the resulting commit is empty or partial.
Reproduced on Claude Code 2.1.89 (macOS).
Relevant configuration
Not machine specifics — these are the conditions that appear to gate the bug:
- Running in a git worktree (
<repo>/.claude/worktrees/<name>), not the primary checkout - No
sandboxkey in project, local, or user settings - A
PreToolUsehook registered onEdit|Write|MultiEdit— a Node script that exits0to allow and2to block, with an early-exit branch for certain exempt paths
Reproduction / isolation matrix
Same session, same worktree, same directory. Each row is an Edit or Write, followed immediately by a Bash grep for the inserted marker:
| Target | Pre-existing? | Git-tracked? | Allowed by hook via | Persisted? |
|---|---|---|---|---|
| notes/<file>.md | yes | yes | early-exit (exempt path) | ✅ yes |
| notes/<new>.txt | no | no | early-exit (exempt path) | ✅ yes |
| lib/<dir>/<new>.js | no | no | full evaluation path | ✅ yes |
| lib/<dir>/a.js | yes | yes | full evaluation path | ❌ no |
| lib/<dir>/b.js | yes | yes | full evaluation path | ❌ no |
| lib/<dir>/_guard.js | yes | yes | full evaluation path | ❌ no |
The last three failed repeatedly and deterministically across ~6 attempts spanning ~40 minutes. The first three succeeded on first try.
The distinguishing factor appears to be pre-existing file + reaching the hook's full evaluation path (vs. its early-exit branch). Creating a new file on that same full path persisted, so it is not simply "hook ran → write dropped".
Observed behaviour
EditreturnsThe file ... has been updated successfully.- A subsequent
Readof the same path shows the edited content (so the edit exists somewhere in-process). grep/git diff/node -c/python3via Bash all show the original content.statreports the pre-edit mtime and byte size.- A
system-reminderlater arrived stating "file was modified, either by the user or by a linter" and quoted the pre-edit content — the same signature reported in #35699. - Writing the identical change with
python3via Bash persisted immediately and was committable.
Ruled out during isolation: extended attributes and file flags (identical to files that wrote fine), .gitattributes filters, open file handles (lsof clean), symlink/path aliasing (realpath correct), a stray second settings file, and read-side sandboxing (dangerouslyDisableSandbox: true on the verifying command changed nothing).
Impact
Silent and high-consequence. In this session it produced a commit that would have been empty had the edit not been independently verified — the model had already been told the edit succeeded and had re-read it as applied. Any workflow that edits and then commits without a git diff check can ship nothing while reporting success.
Workaround
Apply the change through Bash and verify before trusting it:
# write via python3/Bash, with a guard that the match is unique
assert content.count(old) == 1
content = content.replace(old, new)
open(path, 'w').write(content)
then confirm with git diff before committing. Note that Read is not a sufficient check — it returned the phantom edited content throughout.
Suggested fix
Whatever the underlying cause, the Edit/Write tools should fail loudly rather than report success: verify the write landed (re-stat / re-hash after write) and surface an error if it did not. Given three prior issues with this same "reports success, no write" shape closed as NOT_PLANNED, a post-write verification in the tool itself would convert an entire class of silent data loss into a visible error.