Write/Edit tools replace symlinks with regular files, silently breaking dotfile setups
Summary
The Write and Edit tools destroy symlinks instead of writing through them. When targeting a symlinked file (common in dotfile managers), the tools delete the symlink and create a regular file at that path. No warning, no error.
Steps to Reproduce
echo "original content" > /tmp/real-file
ln -s /tmp/real-file /tmp/linked-file
ls -la /tmp/linked-file # Confirm it is a symlink
Use Claude Code Write tool to write to /tmp/linked-file, then:
ls -la /tmp/linked-file # Now a regular file, not a symlink
cat /tmp/real-file # Still has "original content" — untouched
Expected Behavior
The tool should either:
- Resolve symlinks before writing (write to the real target)
- Detect symlinks and warn with the resolved path
- Use
open(path, O_WRONLY|O_TRUNC)instead of atomic temp+rename (which replaces the inode)
Actual Behavior
- The symlink is deleted
- A new regular file is created at the same path
- The original target file is unchanged
- No warning or error is shown
Impact
Affects anyone using symlink-based dotfile managers (dotbot, stow, yadm, chezmoi). The breakage is silent — users discover it later when their dotfiles repo and live config have diverged. Changes made through Claude Code are written to the detached regular file, not tracked in version control.
Workaround
A PreToolUse hook that walks up the path checking each ancestor for symlinks. If found, blocks the write and suggests using the resolved real path:
#\!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[[ -z "$FILE_PATH" ]] && exit 0
p="$FILE_PATH"
while [ "$p" \!= "/" ]; do
if [ -L "$p" ]; then
REAL_PATH=$(readlink -f "$FILE_PATH")
jq -n --arg symlink "$p" --arg target "$(readlink "$p")" --arg real "$REAL_PATH" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"additionalContext": ("BLOCKED: Path passes through symlink: " + $symlink + " -> " + $target + "\nUse the resolved path instead:\n " + $real)
}
}'
exit 0
fi
p=$(dirname "$p")
done
exit 0
Register as a PreToolUse hook matching Write|Edit.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗