[Bug] symlinkDirectories: writing to symlinked file replaces symlink with regular file
Description
When worktree.symlinkDirectories includes a file path (e.g., .claude/settings.local.json), Claude Code correctly creates a symlink at worktree creation time. However, when Claude Code later writes to that file (e.g., to persist a newly approved permission), the symlink is replaced with a regular file. This causes the worktree and the original repo to diverge silently.
Steps to Reproduce
- Configure worktree symlinks in
.claude/settings.json:
``json``
{
"worktree": {
"symlinkDirectories": [
".claude/settings.local.json",
".env",
"node_modules"
]
}
}
- Start a session with
--worktree(or viaEnterWorktree) - Verify
.claude/settings.local.jsonis a symlink → ✅ - During the session, approve a new permission (e.g.,
WebFetch(domain:linear.app)due to #9329) - Claude Code writes the updated permission to
.claude/settings.local.json - Check
.claude/settings.local.jsonagain → no longer a symlink
Expected Behavior
Writing to a symlinked file should write through the symlink, updating the original file. The symlink should remain intact.
Actual Behavior
The symlink is replaced with a regular file. The original repo's settings.local.json is unchanged, while the worktree now has a diverged copy.
# After worktree creation (correct):
$ ls -la .claude/settings.local.json
lrwxr-xr-x ... .claude/settings.local.json -> /path/to/original/.claude/settings.local.json
# After Claude Code writes a new permission (broken):
$ ls -la .claude/settings.local.json
-rw------- ... .claude/settings.local.json # regular file, symlink gone
Evidence: other symlinked paths (.env, .venv, node_modules) that Claude Code does not write to remain intact as symlinks.
Root Cause
Claude Code likely uses an atomic write pattern (write to temp file → rename to target path) when updating settings.local.json. The rename replaces the symlink itself rather than writing through it. This is a common pitfall with symlinks.
Suggested Fix
When writing to a file that is a symlink, either:
- Resolve the symlink first and write to the real path
- Write through the symlink (open with
O_NOFOLLOWdisabled, which is the default) - After an atomic rename, detect that the original was a symlink and restore it
Related Issues
- #40259 — another
symlinkDirectoriesissue (cleanup failure, different root cause) - #9329 — WebFetch wildcard bug that triggers the extra permission write
Environment
- macOS Darwin 24.6.0
- Claude Code CLI (Opus 4.6)
- Worktree created via
EnterWorktree
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗