fix(skills): include plugin skill roots safely - #1573
Open
AaronZ345 wants to merge 1 commit into
Open
Conversation
AaronZ345
force-pushed
the
feat/plugin-skill-roots-depth1-20260720
branch
2 times, most recently
from
July 25, 2026 14:41
7f3869d to
e8fa79e
Compare
AaronZ345
force-pushed
the
feat/plugin-skill-roots-depth1-20260720
branch
2 times, most recently
from
August 7, 2026 14:41
2e8d3a5 to
dbd553c
Compare
AaronZ345
force-pushed
the
feat/plugin-skill-roots-depth1-20260720
branch
2 times, most recently
from
August 14, 2026 14:43
02260d4 to
832c17e
Compare
chenhg5
reviewed
Aug 15, 2026
chenhg5
left a comment
Owner
There was a problem hiding this comment.
Conclusion: Comment
Overall assessment:
- The implementation is fundamentally sound —
agent/internal/skillroots/skillroots.goprovides a correct, symlink-loop-safe scanner, and the Claude Code / CodexSkillDirs()extensions preserve the depth-1 contract enforced bycore.SkillRegistryafter #1317. No blocker found; leaving this asCommentto confirm a couple of design decisions (.claude/skillscross-discovery in Codex,superpowers/skillsdirectory choice) and to surface one minor dedup edge case worth a 5-second author comment rather than a code change.
Review scope:
- Read
agent/internal/skillroots/skillroots.go(+68),agent/claudecode/claudecode.go(+46/-40, SkillDirs path),agent/codex/codex.go(+12/-5, codexSkillDirs + walkUpCodexProjectSkillDirs), and the matching test files (skillroots_test.go+66,claudecode/skilldirs_test.go+80,codex/skilldirs_test.go+80,codex/session_test.go+11). - Focused on correctness of the recursive
Find, symlink/loop safety, integration with the existing depth-1discoverSkillsInDircontract (#1317), and boundary handling for emptypluginDirs, emptycodexHome, and theCLAUDE_CONFIG_DIR=""case. - Ran locally:
go test -race ./agent/internal/skillroots ./agent/codex ./agent/claudecode -count=1 -timeout 90s→ ALL PASS (3/3 packages).go test ./agent/internal/skillroots ./agent/codex ./agent/claudecode ./core→ ALL PASS.go vet ./agent/...clean.go test -tags no_web ./cmd/cc-connect→ PASS. - CI per
gh pr checks: lint, unit-test, smoke-test, regression-test, performance-test all SUCCESS.
✅ What looks good:
- Symlink-loop safety by realpath tracking:
walkkeys bothseenDirsandseenRootsby theEvalSymlinks-resolved path (realDir), not the lexical path. This is the same defensecore.SkillRegistry.ListAlluses (TestSkillRegistryListAll_DoesNotLoopOnDirectorySymlinks), and the newTestFindDoesNotLoopOnSymlinkpins the contract locally. A symlinkedplugin/looppointing back at the walk root cannot produce infinite recursion. - Honoring the depth-1 boundary at the right layer:
Findonly returns<root>/<something>/skillsdirectories; it does not recurse into the discoveredskills/directory or walk into<skill>/references/template/skills/. Combined with the existing depth-1discoverSkillsInDir(which only reads<root>/<skill>/SKILL.md), the phantom-slash-command regression fixed by #1317 cannot reappear. Thenotion/references/template/skillsnegative-assertion in bothTestSkillDirs_IncludesClaudePluginSkillRootsandTestSkillDirs_IncludesCodexPluginSkillRootsproves this end-to-end. - Layering is clean: the scanner lives in
agent/internal/skillroots, not incore, which is correct — it is only consumed by the two agent adapters that own a plugin ecosystem.core.SkillRegistrystays generic and unchanged. - Claude Code
plugin_dircoverage:claudeSkillDirsnow iteratesa.pluginDirs(the resolved list fromopts["plugin_dir"]atclaudecode.go:155-164) and feeds each entry throughskillroots.Find. This composes correctly with #1325's runtime--plugin-dirloader as the PR description claims:--plugin-diris for runtime, this PR is for static discovery — both finally agree on the same plugin roots. a.pluginDirssnapshot under mutex:SkillDirs()copiesa.pluginDirsviaappend([]string(nil), a.pluginDirs...)while holdinga.mu.RLock, matching the pattern already used forworkDirandcodexHome. No data race introduced againstsetPluginDirs-style mutators (none exist today, but the existingTestSkillDirs_RaceFreeAgainstSetWorkDirpattern is the right model).waitForArgsFilesettle window: the newtext != lastdebounce inagent/codex/session_test.go(+6 lines) is a real test-only fix for the partial-write flake. It sleeps 20ms after the first non-empty read and only returns on the second consecutive identical read — this matches the actual partial-write race (writer hasn't flushed yet) and doesn't slow the happy path beyond one extra 20ms tick. Not strictly part of the headline feature, but nice to see bundled in.codexRuntimeConfigTimeoutextension to 5s inTestGetModelAndReasoningEffort_FromRuntimeConfigWhenUnset— the old 1500ms default is too tight for the spawnedcodexshim under parallel CI load, so the test would intermittently fail with "context deadline exceeded". The override +t.Cleanuprestoration is the standard pattern; safe to ship.
🟠 Should improve (P2, non-blocking):
.claude/skillsis silently added to Codex project and home scanning (codex.go:620,codex.go:639). This is a deliberate cross-discovery — a user who writes a~/.claude/skillsskill now sees it surface under Codex too. Two questions:- Is this intentional cross-runtime sharing? Worth a one-line comment in
codexSkillDirsexplaining "Codex can consume Claude-format skill directories for parity with Claude Code skill authors". The current code reads as accidental. - Should the reverse also be true (Codex
.codex/skillsdiscoverable by Claude)? Today the answer is no (claudecode only reads<configHome>/skillsandplugin_dirroots). If the project wants symmetric skill sharing, both adapters should agree on the same set; otherwise users will be confused about why a skill works in one runtime and not the other. Worth deciding.
- Is this intentional cross-runtime sharing? Worth a one-line comment in
codexHome/superpowers/skillsdirectory (codex.go:613).superpowersis a third-party skill orchestration tool (not Codex, not Claude), and this path will silently fail to register anything for the 99% of users without it. Risk is low (the directory just doesn't exist,uniqueCodexSkillDirsskips empty results,SkillRegistry.SetDirsaccepts non-existent dirs without error), but two improvements would be worth it:- A brief comment at
codex.go:613noting this is the superpowers skill layout, so future readers understand why a hardcodedsuperpowers/skillsis there. - A test that asserts
SkillDirs()does not include a<random>/superpowers/skillsfor users who happen to have asuperpowersdirectory in their Codex home that is not the official layout. The existingTestSkillDirs_IncludesCodexPluginSkillRootsdoesn't cover this.
- A brief comment at
Find()accepts the root if and only if its base name isskills, but does not walk into siblings of that root (skillroots.go:13). That's fine for the call sites, but if a future caller passes~/.codex/plugins/<plugin>/skillsdirectly (i.e. a plugin root that is askillsdirectory), the early-return at line 13 returns[root]without dedup-checking against the caller'sseenDirs. Today there are no such callers, so this is theoretical — but worth a one-lineuniqueSkillDirs-style pass at the end ofFindto keep the contract tight.TestFindAcceptsRootNamedSkillsonly exercises the early-return branch (skillroots_test.go:37). The recursivewalkis exercised byTestFindDiscoversNestedSkillRootsandTestFindDoesNotLoopOnSymlink, but no test asserts the recursivewalkhonors the depth-1 contract on a 3-deep nested chain (e.g.root/a/b/c/d/e/skills→ onlye/skillsreturned, neverc/skillsord/skills). Adding one would make the contract self-documenting.
🔵 Optional (P3, do not block):
Find()does not log or surface a warning when the root itself is missing (os.Stat→ error inisDir→ empty result). Callers silently treat an unsetCODEX_HOMEand a missing~/.claude/pluginsidentically. This matches today's behavior and is fine, but a singleslog.DebuginFindwhenrootis empty/missing would help users diagnose "why aren't my plugin skills showing up" without code spelunking. (Moot if you consider this a "config issue, not a runtime issue" — reasonable either way.)CHANGELOG.mddoes not reference the related PRs (#1317 depth-1, #1325 plugin_dir runtime). A "follows #1317 depth-1 contract; complements #1325 plugin_dir loader" parenthetical would help future archeology, but is not required.agent/codex/codex.go:613-615could collapse the two appends into one (userDirs = append(userDirs, filepath.Join(codexHome, "skills"), filepath.Join(codexHome, "superpowers", "skills"))) — purely cosmetic; the current shape documents the plugin-vs-static split. Leave as-is.
❓ Questions:
- For AaronZ345: the
.claude/skillsdiscovery under Codex (codex.go:620,codex.go:639) — is this intentional cross-runtime skill sharing, or a deliberate fallback because Codex's own skill format is still evolving? A short comment in the source would answer this for the next reader without a round-trip. - For maintainer: PR description says "Replaces #1415 with the same approved patch on a fresh branch." Was #1415 reviewed and approved? If so, the QA pass should match the prior verdict. If not, this is effectively a fresh review and the questions above apply.
Testing / Risk:
- Verified evidence: 3 new skillroots unit tests + 2 new claudecode SkillDirs tests + 2 new codex SkillDirs tests all pass under
-race.TestSkillDirs_RaceFreeAgainstSetWorkDir(pre-existing in codex) still passes — the newcodexSkillDirsbody does not introduce new racy reads ofa.workDirora.codexHome.go vet ./agent/...clean. CI 5/5 SUCCESS. - Remaining risk: the new
.claude/skillsandsuperpowers/skillsentries will silently scan directories that may not exist on most users' systems —core.SkillRegistrytolerates missing roots (no error), but if any future code starts asserting "every configured skill dir must exist at startup", this PR would need a rebase. Worth a sanity grep before merge:grep -rn "os.Stat.*skill" core/to confirmSkillRegistry.SetDirsis still tolerant of empty/missing dirs. - The unrelated
agent/acp/TestCursorCLI_ACPHandshakefailure I observed locally is a pre-existing integration-test credential issue (acp: authenticate (cursor_login): json-rpc -32602: Invalid params), not caused by this PR. Confirmed by checking outorigin/mainand re-running the same test — fails identically.
Next step:
- Author: drop a 1-2 line comment in
codex.gonear the.claude/skillsandsuperpowers/skillsentries explaining the intent. Optionally add the 3-deepTestFindAcceptsNestedChaintest. After that, this is mergeable. - Maintainer: once the comment question is resolved, ready to merge.
AaronZ345
force-pushed
the
feat/plugin-skill-roots-depth1-20260720
branch
from
August 15, 2026 14:44
832c17e to
02c75f8
Compare
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.
Replaces #1415 with the same approved patch on a fresh branch.
Summary
skillsroot directories without registering nestedSKILL.mdfiles directlyCODEX_HOME/plugins/**/skills) plus Codex superpowers and.claude/skillscompatibility rootsCLAUDE_CONFIG_DIR/plugins/**/skillsand configuredplugin_dirtreesThis supersedes the approach in #624. That PR is now stale/dirty after #1317, and it reintroduced recursive core skill registration. This keeps #1317's depth-1 registry behavior intact, so nested reference/template
SKILL.mdfiles still stay hidden while plugin skill bundles become discoverable.It also complements #1325:
plugin_dirstill loads Claude plugins at runtime, and this PR makes plugin-provided skill roots visible to cc-connect's static skill/command registry.Tests
git diff --checkgo test ./agent/internal/skillroots ./agent/codex ./agent/claudecode ./corego test -tags no_web ./cmd/cc-connectGOOS=linux go test -tags no_web -c -o /private/tmp/cc-connect-plugin-skill-roots-cmd.test ./cmd/cc-connect