Skip to content

fix(shim): relink dangling shims instead of failing with 'file exists'#64

Merged
gok03 merged 2 commits into
mainfrom
fix/idempotent-shim-relink
Jun 18, 2026
Merged

fix(shim): relink dangling shims instead of failing with 'file exists'#64
gok03 merged 2 commits into
mainfrom
fix/idempotent-shim-relink

Conversation

@gok03

@gok03 gok03 commented Jun 18, 2026

Copy link
Copy Markdown
Member

Problem

Two failure modes from one root cause — both hit in the wild:

  1. refuse install aborts when a shim already exists as a dangling symlink:
    Error: create shim ~/.refuse/bin/npm: symlink ...: file exists
    
  2. Protection silently turns off. A dangling shim (e.g. left after a brew uninstall --cask refuse, with the cask path gone) is skipped by the shell during command resolution, so npm falls through PATH to the real npm and nothing is checked — the user believes Refuse is active when it isn't.

Root cause

Both Install and Uninstall probed the shim with os.Stat, which follows the symlink and returns an error for a broken one:

  • In Install, that error skipped the cleanup branch, so the stale link was never removed before os.Symlink → "file exists".
  • In Uninstall, the ENOENT from a broken link looked like "shim not present", so it was left behind.

Fix (internal/shim/install.go)

  • Install: detect existing entries with os.Lstat (does not follow the link, so dangling shims are seen). Skip only when a valid symlink already resolves to us; otherwise remove the foreign/stale/dangling entry before relinking. Fixes both unix and windows createShim, since the clear happens in the shared caller.
  • Uninstall: use os.Lstat too, and treat a symlink (valid or dangling) that points into our own bin dir as ours, so it gets removed.

Testing

  • go build ./..., go vet ./internal/shim/ — clean.
  • New TestInstallReplacesDanglingShim covers install-over-dangling and uninstall-of-dangling. Passes (with -ldflags=-linkmode=external locally to sidestep an unrelated Go 1.21 / Darwin 25 LC_UUID linker quirk; CI on Linux is unaffected).
  • Verified manually: with all-dangling shims, old refuse install failed with "file exists"; after this change, find ~/.refuse/bin -type l -delete && refuse install relinks cleanly and npm install lodash@4.17.10 is blocked (exit 2).

gok03 added 2 commits June 18, 2026 15:27
`refuse install` probed each shim with os.Stat, which follows the symlink and
errors on a broken one. So a dangling shim — left after the target binary
moved or a Homebrew cask was removed — was never cleared, and os.Symlink then
failed the whole install with:

  Error: create shim .../bin/npm: symlink ...: file exists

Worse, dangling shims silently disable protection: the shell skips broken
symlinks, so `npm` falls through PATH to the real npm and nothing is checked.

- Install: detect existing entries with os.Lstat (does not follow the link),
  skip only when a valid symlink already resolves to us, and remove any
  foreign / stale / dangling entry before relinking.
- Uninstall: likewise use Lstat so a dangling shim pointing into our own bin
  dir is removed too, instead of being treated as already gone.
- Add a regression test covering the dangling-shim case for install + uninstall.
@gok03 gok03 merged commit 63893db into main Jun 18, 2026
8 checks passed
@gok03 gok03 deleted the fix/idempotent-shim-relink branch June 18, 2026 10:21
@gok03 gok03 mentioned this pull request Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant