[GH-ISSUE #3057] [Bug] Setup writes the skill through a symlinked skill directory that uninstall and update skip #1070

Closed
opened 2026-10-15 18:30:49 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @coygeek on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3057

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Setup, packaging, or upgrade

Observed behavior

When a client's reverse-engineer-anything skill directory is a symbolic link to a directory elsewhere (for example a skill manager or dotfiles repository linking ~/.claude/skills/reverse-engineer-anything to a shared copy), rea setup with the skill enabled follows the link and writes REA's skill bundle into the link target, replacing its SKILL.md and creating SKILL.md.rea.backup and references/ there, and doctor then reports the skill aligned. rea uninstall retains the same directory because it is a symbolic link and leaves every written file in the link target, and, from source (not run), rea update does not select a symlinked skill directory for maintenance. REA's skill lifecycle thus disagrees about the same directory: setup and doctor follow the link, while uninstall and update decline it, so setup modifies a location that REA's own removal and maintenance paths will not manage.

Observed with both builds; outputs below are from 6.4.0 unless noted.

  • The dry run returned "status":"planned" with an install_skill:claude_code action targeting <HOME>/.claude/skills/reverse-engineer-anything ("operation":"install", detail "Install or update the REA skill in Claude Code's personal skill directory."), with no mention of the link.
  • --skill=false --yes returned "status":"ready" and left managed/rea unchanged.
  • --yes returned "status":"ready" and "appliedActions":["installed_skill"], and its doctor block reported the skill "state":"aligned" (installed_version "37", installed_tool_count 170; main: "41" and 206). The link itself was kept, but managed/rea now held REA's packaged SKILL.md, a SKILL.md.rea.backup whose SHA-256 matched the original SKILL.md, and six references/*.md files.
  • uninstall --json returned "status":"complete" with the skill item "status":"retained" and a combined per-destination detail including Claude Code skill: retained (Skill directory is a symbolic link.) (main includes claude_code (<HOME>/.claude/skills/reverse-engineer-anything): retained (Skill directory is a symbolic link.)). All eight files remained in managed/rea.
  • Control (regular directory, 6.4.0): setup wrote into .claude/skills/reverse-engineer-anything, managed/rea stayed unchanged, and uninstall's skill detail included Claude Code skill: removed (REA-managed skill files were removed; other files were preserved.).

Reproduction

Run with the 6.4.0 package (replace npx -y rea-agents@6.4.0 with node <checkout>/scripts/rea.mjs for the main build):

export HOME="$(mktemp -d)"   # isolated home; CLAUDE_CONFIG_DIR unset
cd "$HOME"
mkdir -p .claude/skills managed/rea
printf -- '---\nname: reverse-engineer-anything\ndescription: user-managed copy\n---\n\nUser-managed skill body.\n' > managed/rea/SKILL.md
ln -s ../../managed/rea .claude/skills/reverse-engineer-anything

npx -y rea-agents@6.4.0 setup --client claude_code --dry-run --json
npx -y rea-agents@6.4.0 setup --client claude_code --skill=false --yes --json
find managed/rea -type f | sort      # still only SKILL.md
npx -y rea-agents@6.4.0 setup --client claude_code --yes --json
ls -l .claude/skills; find managed/rea -type f | sort; head -7 managed/rea/SKILL.md
npx -y rea-agents@6.4.0 uninstall --json
find managed/rea -type f | sort

Control: repeat with mkdir .claude/skills/reverse-engineer-anything and a copy of the same SKILL.md in place of the ln -s line.

Expected behavior

Taken together, these passages in docs/installation.md on main describe one set of REA-owned skill destinations across setup, uninstall and update: Skill destinations (line 114) says "Setup leaves existing skill copies in other locations untouched."; Diagnose, update, and remove (line 162) says rea uninstall "removes only REA-owned agent registrations and skill files", and lines 170-176 say a symbolic-link purge path "is retained and reported rather than followed"; and the update maintenance plan (lines 197-198) "selects only existing REA registrations and an already installed REA skill". Uninstall applies the symbolic-link rule to skill directories ("Skill directory is a symbolic link."), and the closed morluto/rea#1816 applied it to a symlinked ~/.rea root. For the same symlinked skill directory, setup, doctor, uninstall and update should therefore agree: either setup declines the link (leaving the target untouched and reporting why), or REA treats the link target as its managed destination throughout, so uninstall removes the REA-managed files setup wrote there and update maintains them. Today setup writes and doctor reports aligned, while uninstall and update (from source) decline the directory.

REA already follows user-owned symbolic links for client configuration files on purpose (src/application/ClientConfigPath.ts, used by both setup and uninstall), so either direction may fit the project's design; the defect is that the skill paths disagree with each other, not a requirement to pick one direction.

Impact and scope

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: For users whose skill directory is a symbolic link (observed for the Claude Code destination), setup replaces files in a directory outside REA's skill roots, such as a shared or version-controlled skill copy, and REA's uninstall reports the skill retained and leaves the written files; the original SKILL.md from that run survives as SKILL.md.rea.backup, so the change is recoverable by hand.
  • Workaround status: available
  • Workaround basis: Running setup with --skill=false left the link target unchanged in the reproduction (observed) while still configuring the client registration; installing the skill by another route afterwards is a user choice that was not tested here.

REA version

6.4.0 (npm latest) and main at e049ef70f7

Runtime and platform

Node.js 26.10.0, npm 12.2.0, macOS 27.0 arm64; each run used a fresh temporary HOME with CLAUDE_CONFIG_DIR and the other client home variables unset.

Target and artifact context

Not applicable (setup and uninstall of the bundled skill).

Interface

Package or setup

Earliest failure stage

Setup or provider admission

Relevant output or evidence

  • Expected source: docs/installation.md Skill destinations ("Setup leaves existing skill copies in other locations untouched.") and Diagnose, update, and remove (uninstall removes only REA-owned skill files; symbolic-link paths are retained rather than followed), and the update maintenance plan (lines 197-198: it "selects only existing REA registrations and an already installed REA skill"), with the matching paragraphs at lines 354, 847, 860 and 426-427 of the 6.4.0 tag; src/application/ClientConfigPath.ts (resolveClientConfigTransactionPath, the user-owned configuration symlink precedent); src/application/Uninstall.ts:683-684 on main (symlinked skill directory retained); src/application/SetupSkill.ts:208-210 on main (existingSkillDestinations skips a symlinked root; src/application/UpdateMaintenance.ts:73-83 uses it to choose the skill destinations rea update maintains). The 6.4.0 source has the same root check.
  • Failure source: the setup and uninstall runs above on both builds. In source, installCanonicalSkill (src/application/SetupSkill.ts:339-405 on main) creates directories, backs up and writes each managed file under the destination root without checking whether the root is a symbolic link; the symlinks: "reject" option on skill file reads applies to the file itself, not to a linked parent directory.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The rea update behavior is static: it is cited from source and was not run, because it requires an npm or Bun global installation. The reproduction covers the Claude Code destination; the install path is shared by every skill destination, but other destinations were not run. Setup also replaces a non-REA SKILL.md in a regular skill directory (see the control), which is a separate ownership question; this report concerns only following the link.

Typed outcome or recovery expected

Failing now: after the setup --yes step, managed/rea contains REA's SKILL.md, SKILL.md.rea.backup and six references/*.md files, doctor reports the skill aligned, and uninstall reports the Claude Code skill retained (Skill directory is a symbolic link.) and leaves all of them. Restored, by either consistent outcome for the same symlinked directory: setup leaves managed/rea byte-identical and reports the Claude Code skill destination as skipped or refused with a reason; or setup's write is matched by uninstall removing the REA-managed files it wrote in managed/rea (preserving the user's other files) and by update selecting that destination. Control: with a regular ~/.claude/skills/reverse-engineer-anything directory, setup still installs the skill and uninstall still removes REA's managed files.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3057 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Setup, packaging, or upgrade ### Observed behavior When a client's `reverse-engineer-anything` skill directory is a symbolic link to a directory elsewhere (for example a skill manager or dotfiles repository linking `~/.claude/skills/reverse-engineer-anything` to a shared copy), `rea setup` with the skill enabled follows the link and writes REA's skill bundle into the link target, replacing its `SKILL.md` and creating `SKILL.md.rea.backup` and `references/` there, and doctor then reports the skill `aligned`. `rea uninstall` retains the same directory because it is a symbolic link and leaves every written file in the link target, and, from source (not run), `rea update` does not select a symlinked skill directory for maintenance. REA's skill lifecycle thus disagrees about the same directory: setup and doctor follow the link, while uninstall and update decline it, so setup modifies a location that REA's own removal and maintenance paths will not manage. Observed with both builds; outputs below are from 6.4.0 unless noted. - The dry run returned `"status":"planned"` with an `install_skill:claude_code` action targeting `<HOME>/.claude/skills/reverse-engineer-anything` (`"operation":"install"`, detail "Install or update the REA skill in Claude Code's personal skill directory."), with no mention of the link. - `--skill=false --yes` returned `"status":"ready"` and left `managed/rea` unchanged. - `--yes` returned `"status":"ready"` and `"appliedActions":["installed_skill"]`, and its doctor block reported the skill `"state":"aligned"` (`installed_version` "37", `installed_tool_count` 170; `main`: "41" and 206). The link itself was kept, but `managed/rea` now held REA's packaged `SKILL.md`, a `SKILL.md.rea.backup` whose SHA-256 matched the original `SKILL.md`, and six `references/*.md` files. - `uninstall --json` returned `"status":"complete"` with the skill item `"status":"retained"` and a combined per-destination detail including `Claude Code skill: retained (Skill directory is a symbolic link.)` (`main` includes `claude_code (<HOME>/.claude/skills/reverse-engineer-anything): retained (Skill directory is a symbolic link.)`). All eight files remained in `managed/rea`. - Control (regular directory, 6.4.0): setup wrote into `.claude/skills/reverse-engineer-anything`, `managed/rea` stayed unchanged, and uninstall's skill detail included `Claude Code skill: removed (REA-managed skill files were removed; other files were preserved.)`. ### Reproduction Run with the 6.4.0 package (replace `npx -y rea-agents@6.4.0` with `node <checkout>/scripts/rea.mjs` for the `main` build): ```sh export HOME="$(mktemp -d)" # isolated home; CLAUDE_CONFIG_DIR unset cd "$HOME" mkdir -p .claude/skills managed/rea printf -- '---\nname: reverse-engineer-anything\ndescription: user-managed copy\n---\n\nUser-managed skill body.\n' > managed/rea/SKILL.md ln -s ../../managed/rea .claude/skills/reverse-engineer-anything npx -y rea-agents@6.4.0 setup --client claude_code --dry-run --json npx -y rea-agents@6.4.0 setup --client claude_code --skill=false --yes --json find managed/rea -type f | sort # still only SKILL.md npx -y rea-agents@6.4.0 setup --client claude_code --yes --json ls -l .claude/skills; find managed/rea -type f | sort; head -7 managed/rea/SKILL.md npx -y rea-agents@6.4.0 uninstall --json find managed/rea -type f | sort ``` Control: repeat with `mkdir .claude/skills/reverse-engineer-anything` and a copy of the same `SKILL.md` in place of the `ln -s` line. ### Expected behavior Taken together, these passages in `docs/installation.md` on `main` describe one set of REA-owned skill destinations across setup, uninstall and update: Skill destinations (line 114) says "Setup leaves existing skill copies in other locations untouched."; Diagnose, update, and remove (line 162) says `rea uninstall` "removes only REA-owned agent registrations and skill files", and lines 170-176 say a symbolic-link purge path "is retained and reported rather than followed"; and the update maintenance plan (lines 197-198) "selects only existing REA registrations and an already installed REA skill". Uninstall applies the symbolic-link rule to skill directories ("Skill directory is a symbolic link."), and the closed morluto/rea#1816 applied it to a symlinked `~/.rea` root. For the same symlinked skill directory, setup, doctor, uninstall and update should therefore agree: either setup declines the link (leaving the target untouched and reporting why), or REA treats the link target as its managed destination throughout, so uninstall removes the REA-managed files setup wrote there and update maintains them. Today setup writes and doctor reports `aligned`, while uninstall and update (from source) decline the directory. REA already follows user-owned symbolic links for client configuration files on purpose (`src/application/ClientConfigPath.ts`, used by both setup and uninstall), so either direction may fit the project's design; the defect is that the skill paths disagree with each other, not a requirement to pick one direction. ### Impact and scope - Impact level: P2 - Assessment status: supported - Impact basis: For users whose skill directory is a symbolic link (observed for the Claude Code destination), setup replaces files in a directory outside REA's skill roots, such as a shared or version-controlled skill copy, and REA's uninstall reports the skill retained and leaves the written files; the original `SKILL.md` from that run survives as `SKILL.md.rea.backup`, so the change is recoverable by hand. - Workaround status: available - Workaround basis: Running setup with `--skill=false` left the link target unchanged in the reproduction (observed) while still configuring the client registration; installing the skill by another route afterwards is a user choice that was not tested here. ### REA version 6.4.0 (npm `latest`) and `main` at e049ef70f71aeb9965e9d75d4d4a84ea02eaee11 ### Runtime and platform Node.js 26.10.0, npm 12.2.0, macOS 27.0 arm64; each run used a fresh temporary `HOME` with `CLAUDE_CONFIG_DIR` and the other client home variables unset. ### Target and artifact context Not applicable (setup and uninstall of the bundled skill). ### Interface Package or setup ### Earliest failure stage Setup or provider admission ### Relevant output or evidence - Expected source: `docs/installation.md` Skill destinations ("Setup leaves existing skill copies in other locations untouched.") and Diagnose, update, and remove (uninstall removes only REA-owned skill files; symbolic-link paths are retained rather than followed), and the update maintenance plan (lines 197-198: it "selects only existing REA registrations and an already installed REA skill"), with the matching paragraphs at lines 354, 847, 860 and 426-427 of the 6.4.0 tag; `src/application/ClientConfigPath.ts` (`resolveClientConfigTransactionPath`, the user-owned configuration symlink precedent); `src/application/Uninstall.ts:683-684` on `main` (symlinked skill directory retained); `src/application/SetupSkill.ts:208-210` on `main` (`existingSkillDestinations` skips a symlinked root; `src/application/UpdateMaintenance.ts:73-83` uses it to choose the skill destinations `rea update` maintains). The 6.4.0 source has the same root check. - Failure source: the setup and uninstall runs above on both builds. In source, `installCanonicalSkill` (`src/application/SetupSkill.ts:339-405` on `main`) creates directories, backs up and writes each managed file under the destination root without checking whether the root is a symbolic link; the `symlinks: "reject"` option on skill file reads applies to the file itself, not to a linked parent directory. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The `rea update` behavior is static: it is cited from source and was not run, because it requires an npm or Bun global installation. The reproduction covers the Claude Code destination; the install path is shared by every skill destination, but other destinations were not run. Setup also replaces a non-REA `SKILL.md` in a regular skill directory (see the control), which is a separate ownership question; this report concerns only following the link. ### Typed outcome or recovery expected Failing now: after the setup `--yes` step, `managed/rea` contains REA's `SKILL.md`, `SKILL.md.rea.backup` and six `references/*.md` files, doctor reports the skill `aligned`, and uninstall reports the Claude Code skill `retained (Skill directory is a symbolic link.)` and leaves all of them. Restored, by either consistent outcome for the same symlinked directory: setup leaves `managed/rea` byte-identical and reports the Claude Code skill destination as skipped or refused with a reason; or setup's write is matched by uninstall removing the REA-managed files it wrote in `managed/rea` (preserving the user's other files) and by update selecting that destination. Control: with a regular `~/.claude/skills/reverse-engineer-anything` directory, setup still installs the skill and uninstall still removes REA's managed files.
bsakaguchi 2026-10-15 18:30:49 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@akushonkamen commented on GitHub (Oct 14, 2026):

Claiming this for a fix.

Scope I am taking — the documented direction (skill symbolic-link paths are retained and reported rather than followed, docs/installation.md "Diagnose, update, and remove", matching rea uninstall's existing rule and the closed #1816 precedent for the ~/.rea root):

  • rea setup stops writing through a symbolic-link skill destination root: canonicalSkillNeedsInstall and installCanonicalSkill (src/application/SetupSkill.ts) treat a linked root as user-managed and skip it, so the link target stays byte-identical and no install_skill action is planned or applied.
  • rea doctor stops comparing through the link: readInstalledSkillIdentity ignores symbolic-link roots, and doctor reports a new skill state for an all-links selection with a reason and remediation (replace the link with a real directory), instead of aligned after a setup that no longer writes, or a misleading missing/"run rea setup" loop.

Out of scope, so nobody duplicates: uninstall and update behavior (both already decline linked roots; unchanged), the alternative direction of managing files inside the link target (that would reverse the documented retention policy and needs a maintainer call), any change to client configuration file symlink following (ClientConfigPath.ts), and the separate ownership question of setup replacing a non-REA SKILL.md in a regular directory.

Regression tests will synthesize a linked Claude Code skill destination per the report's reproduction; the setup/uninstall/doctor lifecycle stays covered by the existing regular-directory tests. PR to follow on top of current main.

<!-- gh-comment-id:6159542053 --> @akushonkamen commented on GitHub (Oct 14, 2026): Claiming this for a fix. Scope I am taking — the documented direction (skill symbolic-link paths are retained and reported rather than followed, docs/installation.md "Diagnose, update, and remove", matching `rea uninstall`'s existing rule and the closed #1816 precedent for the `~/.rea` root): - `rea setup` stops writing through a symbolic-link skill destination root: `canonicalSkillNeedsInstall` and `installCanonicalSkill` (`src/application/SetupSkill.ts`) treat a linked root as user-managed and skip it, so the link target stays byte-identical and no `install_skill` action is planned or applied. - `rea doctor` stops comparing through the link: `readInstalledSkillIdentity` ignores symbolic-link roots, and doctor reports a new skill state for an all-links selection with a reason and remediation (replace the link with a real directory), instead of `aligned` after a setup that no longer writes, or a misleading `missing`/"run rea setup" loop. Out of scope, so nobody duplicates: uninstall and update behavior (both already decline linked roots; unchanged), the alternative direction of managing files inside the link target (that would reverse the documented retention policy and needs a maintainer call), any change to client *configuration* file symlink following (`ClientConfigPath.ts`), and the separate ownership question of setup replacing a non-REA `SKILL.md` in a regular directory. Regression tests will synthesize a linked Claude Code skill destination per the report's reproduction; the setup/uninstall/doctor lifecycle stays covered by the existing regular-directory tests. PR to follow on top of current main.
Author
Owner

@jpdefo commented on GitHub (Oct 14, 2026):

I independently reproduced this on current main b3c5672 with the real CLI on native Windows while the claim above was being posted. I have a locally tested candidate, but will hold off opening a competing PR.

Evidence and review notes that may help:

  • A real directory symlink reproduces the published report on current main: setup reports aligned and writes the bundle through the link, while uninstall retains those files. A regular-directory control installs and removes managed files normally. --skill=false leaves the linked target unchanged.
  • A canonical bundle behind the link needs a control too. Merely refusing changed writes misses the already-aligned/unchanged path. Dangling links and Windows directory junctions also exercise the root ownership check.
  • In the refusal direction, checking only inside installCanonicalSkill is too late for a combined setup: client registration is configured first. Planning/dry-run should disclose the outcome before client or Hopper changes, and the installer should retain its own check.
  • Interactive setup first constructs proposals from all detected/default clients before selection. Applying a blocking check to that proposal set can erase the Hopper and CLI-only skill choices because of a linked client the user would not select. Check the final selected roots and preserve unrelated proposal choices.

Our local candidate exercised the compiled setup/uninstall CLI and native filesystem, plus workflow cases for selecting a linked Claude root, an unaffected Codex root, or CLI-only setup. The candidate's focused checks passed (85 tests, one POSIX-only skip), as did npm run check and npm run docs:check. These results apply to our local candidate, not to your forthcoming implementation. Two existing Windows path-separator assertions in Setup.planning.test.ts also failed identically on the unchanged baseline.

<!-- gh-comment-id:6159684169 --> @jpdefo commented on GitHub (Oct 14, 2026): I independently reproduced this on current main `b3c5672` with the real CLI on native Windows while the claim above was being posted. I have a locally tested candidate, but will hold off opening a competing PR. Evidence and review notes that may help: - A real directory symlink reproduces the published report on current main: setup reports aligned and writes the bundle through the link, while uninstall retains those files. A regular-directory control installs and removes managed files normally. `--skill=false` leaves the linked target unchanged. - A canonical bundle behind the link needs a control too. Merely refusing changed writes misses the already-aligned/unchanged path. Dangling links and Windows directory junctions also exercise the root ownership check. - In the refusal direction, checking only inside `installCanonicalSkill` is too late for a combined setup: client registration is configured first. Planning/dry-run should disclose the outcome before client or Hopper changes, and the installer should retain its own check. - Interactive setup first constructs proposals from all detected/default clients before selection. Applying a blocking check to that proposal set can erase the Hopper and CLI-only skill choices because of a linked client the user would not select. Check the final selected roots and preserve unrelated proposal choices. Our local candidate exercised the compiled setup/uninstall CLI and native filesystem, plus workflow cases for selecting a linked Claude root, an unaffected Codex root, or CLI-only setup. The candidate's focused checks passed (85 tests, one POSIX-only skip), as did `npm run check` and `npm run docs:check`. These results apply to our local candidate, not to your forthcoming implementation. Two existing Windows path-separator assertions in `Setup.planning.test.ts` also failed identically on the unchanged baseline.
Author
Owner

@akushonkamen commented on GitHub (Oct 14, 2026):

Opened PR #3109: setup and doctor now follow the documented retention rule uninstall already applied — linked skill roots are partitioned out (lstat), all-links selections plan no install and stay byte-identical, mixed selections write only managed roots, and doctor reports a new unmanaged skill state naming the link with a replace-the-link remediation instead of a run rea setup loop. Five new symlink regressions fail on pre-fix code (5 failed | 10 passed) and pass after (15/15); the related 15-file setup/doctor/update subset passes 197 tests. Note: PR #3108 also targets this issue with a refusal-framed variant, opened after the claim here — left for maintainer arbitration.

<!-- gh-comment-id:6159893026 --> @akushonkamen commented on GitHub (Oct 14, 2026): Opened PR #3109: setup and doctor now follow the documented retention rule uninstall already applied — linked skill roots are partitioned out (lstat), all-links selections plan no install and stay byte-identical, mixed selections write only managed roots, and doctor reports a new `unmanaged` skill state naming the link with a replace-the-link remediation instead of a `run rea setup` loop. Five new symlink regressions fail on pre-fix code (5 failed | 10 passed) and pass after (15/15); the related 15-file setup/doctor/update subset passes 197 tests. Note: PR #3108 also targets this issue with a refusal-framed variant, opened after the claim here — left for maintainer arbitration.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1070
No description provided.