Write/Edit tools replace symlinks with regular files, silently breaking dotfile setups

Resolved 💬 4 comments Opened Feb 25, 2026 by m-gris Closed Mar 25, 2026

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.

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗