Skip to content

fix(skills): warn and skip the sweep of a symlinked skills root instead of failing - #3299

Merged
dyoshikawa merged 2 commits into
mainfrom
resolve-issue-3284
Oct 3, 2026
Merged

dyoshikawa merged 2 commits into
mainfrom
resolve-issue-3284

Conversation

@dyoshikawa

@dyoshikawa dyoshikawa commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

rulesync generate --global --delete exited 1 when a tool's skills root was a symbolic link, even one that stays inside the output root (e.g. ~/.cursor/skills -> ~/dotfiles/cursor-skills). The generated skills were already written through the link, and then the orphan sweep threw Refusing to write through a symbolic link, failing the run (also under --dry-run).

SkillsProcessor.loadExistingSkillsRoots now catches the guard's refusal, logs Skipping the orphan sweep for <root>; nothing under it is deleted: ..., and leaves that root out of both halves of the sweep. The run exits 0 and writes still land.

The per-directory orphan file sweep (DirFeatureProcessor.removeOrphanFilesInAiDirs) now applies the same guard to each candidate's root, not only to the candidate directory. Without that, it would still sweep the generated skill directories through the linked root. The same setup for subagents (~/.claude/agents linked into a dotfiles checkout) also failed the run, so SubagentsProcessor gets the same warn-and-skip.

Decision: keep refusing to sweep through the link (option 2 from the issue thread)

The issue thread offered two options: (1) sweep through a link that stays inside the output root, or (2) keep refusing, but warn and skip that root's sweep instead of failing the run. This PR implements option 2.

Rationale: a link inside the output root can still point at a directory rulesync does not manage, such as a project folder or another tool's skills tree. Sweeping through it would delete those entries as orphans. The e2e test from #2363 (.kimi-code/skills linked to a project directory) pins that deletion as forbidden. Option 2 keeps that safety property. It only changes how the refusal is reported: a warning instead of a fatal error raised after the writes have already landed. This also matches how a symlinked skill directory inside a real skills root is already handled (warn and skip).

Changes

  • src/features/skills/skills-processor.ts: warn and skip a skills root that fails assertWritablePathInsideRoot instead of throwing.
  • src/types/dir-feature-processor.ts: guard the candidate's root as well before sweeping files inside a generated directory.
  • src/features/subagents/subagents-processor.ts: warn and skip a symlinked subagents root instead of throwing.
  • src/e2e/e2e-skills.spec.ts: the feat: add Kimi Code support #2363 test now expects the run to succeed with the warning, and still checks that the protected skill is kept. A new global e2e test covers the issue's reproduction (.cursor/skills linked inside the home dir) with and without --dry-run: exit 0, the warning is logged, the stale skill and a hand-placed file inside a generated skill directory are kept, and the new skill is written only without --dry-run.
  • src/e2e/e2e-subagents.spec.ts: global e2e test for a linked ~/.claude/agents.
  • src/e2e/e2e-helper.ts: runGenerate gains a dryRun option.
  • src/features/skills/skills-processor.test.ts: unit test for the skipped root.
  • docs/reference/file-formats.md (+ regenerated docs-content.ts): documents the --delete behavior for a linked skills or subagents directory.

Out of scope: when a parent directory is the link (e.g. ~/.cursor -> ~/dotfiles/cursor), or when the link is a commands directory, the guard still sweeps through it. That behavior predates this PR and is unchanged here.

Closes #3284

🤖 Generated with Claude Code

https://claude.ai/code/session_01Y4MwhqMc6s4ct5MzMReGJj

dyoshikawa and others added 2 commits October 2, 2026 20:32
generate --delete threw when a tool's skills root was a symbolic link,
even one staying inside the output root (a dotfiles checkout linked
from the home directory), failing the run after the writes had landed.
The root's orphan sweep is now skipped with a warning instead; it is
still not swept through the link, so entries outside rulesync-managed
outputs are never deleted.

Closes #3284

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4MwhqMc6s4ct5MzMReGJj
Skipping the skills root in the directory sweep let the per-directory
orphan file sweep run, and it still swept generated skill directories
through the linked root, deleting hand-placed files the warning said were
kept. Guard each candidate's root there as well. Apply the same warn and
skip to a symlinked subagents root, which still failed the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y4MwhqMc6s4ct5MzMReGJj
@dyoshikawa
dyoshikawa merged commit f09cb54 into main Oct 3, 2026
9 checks passed
@dyoshikawa

Copy link
Copy Markdown
Owner Author

@dyoshikawa Thank you!

@dyoshikawa
dyoshikawa deleted the resolve-issue-3284 branch October 3, 2026 04:19
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.

generate --global --delete exits 1 when a tool's skills directory is a symlink inside the home directory

1 participant