fix(installer): stop swapping symlinked config files for detached copies (shared AGENTS.md setups) - #1503
Open
meganemura wants to merge 1 commit into
Conversation
…ies (shared AGENTS.md setups) atomicWriteFileSync landed its temp file on the destination via renameSync, which replaces a symlink itself rather than its target. Setups that symlink one shared instructions file into each agent's expected location (~/.claude/CLAUDE.md -> ~/AGENTS.md, ~/.codex/AGENTS.md -> ~/AGENTS.md, ...), and dotfiles-managed configs, silently lost the link: the file became a detached regular-file copy, and later edits to the shared source never reached the file the agent actually reads. Resolve the symlink chain first (manually — realpathSync throws on dangling links, and creating a dangling link's target must keep working) and run the temp-file-plus-rename against the real target, keeping the write atomic on the target's own filesystem. When several selected agents point at the same shared file, the marker-based upsert dedupes across them: the guidance block is written exactly once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
atomicWriteFileSynclands its temp file on the destination viarenameSync, which replaces a symlink itself rather than its target. That silently breaks the common multi-agent setup where one shared instructions file is symlinked into each agent's expected location:After
codegraph install, whichever of these the installer touched is a regular-file copy: that agent's instructions file stops tracking the sharedAGENTS.md, and the copies drift apart on the next edit. With the installer now covering eight agents, the people most likely to run it — users wiring up several agents at once — are exactly the people running this kind of setup. The same failure hits project-local installs (e.g. a repo-rootGEMINI.md -> AGENTS.mdlink — the local Gemini instructions file lives at the project root) and dotfiles-managed configs, and it isn't limited to markdown: every installer write (JSON and TOML configs included) funnels through this helper, so the one fix covers them all.The fix: resolve the symlink chain first, then run the temp-file-plus-rename against the real target — the write stays atomic on the target's own filesystem, and the link survives. When several selected agents are symlinked to the same shared file, the marker-based upsert then dedupes across them: the first install writes the block, the rest report
unchanged(same guarantee gemini.ts already documents for Gemini + Antigravity sharing GEMINI.md, extended through symlinks).For context, this wasn't always broken: the original installer used a plain
writeFileSync(which follows symlinks). The tmp+rename pattern arrived with the crash-safety hardening in 399d78b, and the symlink side effect went unnoticed — a classic pitfall of atomic-save implementations.Relationship to #433
@0x1306a94 diagnosed this exact problem back in May and proposed the same core fix in #433 — full credit to them for getting there first; the CHANGELOG entry here thanks them accordingly. I'm proposing this PR as an alternative because the two differ in a few places beyond base-branch drift, and I'd argue for these choices:
realpathSyncwith a single-hopreadlinkSyncfallback for dangling links, which leaves a multi-hop dangling chain (link → link → not-yet-created target) partially resolved — the rename would still replace the second link. This PR walks the chain manually from the start (32-hop cap, mirroring kernel ELOOP limits), so existing and dangling links take the same code path.removeMarkedSection's empty-file cleanup, which makes uninstall delete the link's target — a file the user manages in their dotfiles repo — and leaves dangling links behind. This PR keeps the current behavior (the link is removed, the target survives): between the two imperfect options, deleting a user-managed file seemed like the worse failure mode. Happy to adjust if maintainers prefer otherwise.replaceOrAppendMarkedSectionwith local marker constants. The tests here reproduce the reported scenario against the real surface: a dotfiles-managedCLAUDE.mdsymlink throughupsertInstructionsEntry→ re-run returnsunchanged→removeMarkedSection, with the link and user content intact at every step — plus chain, dangling-link,writeJsonFile, no-leftover-tmp-file, and a multi-select case (two agents symlinked to one sharedAGENTS.mdget the block exactly once; the second upsert isunchanged). All symlink tests are POSIX-gated per the repo's Windows-gated-tests convention.Test plan
npx vitest run __tests__/installer-targets.test.ts— 171 tests pass (165 existing + 6 new symlink tests), macOStsc --noEmitclean🤖 Generated with Claude Code