Skip to content

fix(parser): deduplicate component dirs when manifest declares the default - #1260

Open
danikdanik wants to merge 1 commit into
EveryInc:mainfrom
danikdanik:fix/declare-skills-dir-in-claude-manifest
Open

fix(parser): deduplicate component dirs when manifest declares the default#1260
danikdanik wants to merge 1 commit into
EveryInc:mainfrom
danikdanik:fix/declare-skills-dir-in-claude-manifest

Conversation

@danikdanik

@danikdanik danikdanik commented Jul 26, 2026

Copy link
Copy Markdown

Summary

  • Deduplicate by resolved path in resolveComponentDirs so a manifest that declares the default directory explicitly (e.g. "skills": "./skills/") does not scan it twice and double-count skills
  • Add a regression fixture (default-dir-declared) with a manifest declaring "skills": "./skills/" and two skills, asserting the parser returns 2 skills, not 4

Context

resolveComponentDirs always included the default dir (root/skills) AND any manifest-declared dir. When a manifest declares the default dir explicitly, the default dir ended up in the array twice, producing 2N skills in conversion output. This was latent on main because the Claude manifest did not declare the skills field, but it is reachable for any plugin that does.

The original version of this PR also added "skills": "./skills/" to .claude-plugin/plugin.json based on a claim that the Claude Code loader recursively scanned the entire repo tree without it. That claim was wrong — the loader scans skills/<name>/SKILL.md one level deep and does not pick up fixtures under tests/fixtures/. The manifest change has been dropped. Thanks to @tmchow for the thorough review that caught this.

Test plan

  • bun run test — 2747 pass, 0 fail
  • bun run plugin:validate — both manifests pass --strict
  • bun run release:validate — in sync, 32 skills
  • Regression test fails without the fix (returns 4 skills), passes with it (returns 2)

Security Disclosure

No security-relevant changes.

Agent Disclosure

Claude Code · claude-sonnet-4-5 (verification and code changes); review and reframing informed by prior sessions on Claude Code 2.1.220.

@tmchow

tmchow commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks @danikdanik for the contribution!

I'd like to reconcile the stated problem with what I measured, because I couldn't reproduce the recursive-scan behavior.

What I tested (Claude Code 2.1.220)

I built a throwaway local marketplace plugin myplug with two SKILL.md files:

myplug/.claude-plugin/plugin.json      # no "skills" field
myplug/skills/real-alpha/SKILL.md
myplug/tests/fixtures/deep/fake-beta/SKILL.md

Installed it project-scoped, then asked a fresh session what it could see:

$ claude -p "List every available skill name you can see that starts with 'myplug'." --model claude-haiku-4-5-20251001
myplug:real-alpha

fake-beta was not loaded. Re-running with "skills": "./skills/" added to the manifest (verified the cache picked up the new version) produced the identical result — myplug:real-alpha, no duplicates.

Corroborating evidence from a real install: my ~/.claude/plugins/cache/compound-engineering-plugin/compound-engineering/3.20.0/.claude-plugin/plugin.json has no skills field, and that tree does contain all 6 fixture SKILL.md files — yet none of skill-one, custom-skill, default-skill, claude-only-skill, disabled-skill, or agent-only-skill show up in the session's skill list.

Question 1: how did you observe the 6 fixture skills loading? The test plan says "Verified the 6 fixture skills no longer load with the manifest field present" — that's consistent with them also not loading without it. If you have a before-state capture (a /skills listing, a screenshot, a Claude Code version), that would settle it. If the loader really does recurse on some version/platform I'd like to know which, since that changes what the right fix is.

Question 2 — the parser change is the part I can reproduce, and it's worth landing. On main, with a 2-skill plugin:

manifest skills skills parsed
(absent) 2
"./skills/" 4 (duplicated)
"skills" 4 (duplicated)

On this branch all three return 2. Note the ordering though: that double-scan is latent on main and only becomes reachable because this PR adds the manifest field. Could you add a regression test? Nothing currently covers "manifest declares the default dir" — the custom-paths fixture only declares non-default dirs (./custom-skills), which is why CI stayed green. A small fixture asserting a declared ./skills/ yields N and not 2N would have failed before your patch.

Question 3 — validation claims. The branch is based on f8e90d11 (2026-06-28), which predates 1a7a4c1e making root CLAUDE.md a symlink to AGENTS.md. On this branch as checked out, CLAUDE.md is a regular file (100644), and:

$ claude plugin validate .claude-plugin/plugin.json --strict
⚠ root: CLAUDE.md at the plugin root is not loaded as project context...
✘ Validation failed (--strict treats warnings as errors)

Separately, claude plugin validate . (as written in the test plan) resolves this repo as a marketplace only and skips plugin-root checks — see the "Maintaining plugin:validate" section in AGENTS.md; that's why package.json validates the two manifests explicitly. Could you rebase onto current main and re-run bun run test and bun run plugin:validate? The "1599 pass" figure is from the month-old base; main now runs 2730 tests.

Merging the PR into current main locally does give release:validate in sync (32 skills) and a clean --strict pass, so this is a rebase/description issue rather than a conflict.

…fault

resolveComponentDirs always included the default dir (root/skills) AND
any manifest-declared dir. When a manifest declares the default dir
explicitly (e.g. `"skills": "./skills/"`), this scanned it twice and
double-counted skills. Deduplicate by resolved path.

Adds a regression fixture (default-dir-declared) with a manifest that
declares `"skills": "./skills/"` and two skills, asserting the parser
returns 2 skills, not 4.
@danikdanik
danikdanik force-pushed the fix/declare-skills-dir-in-claude-manifest branch from 912faa9 to bdda198 Compare July 29, 2026 12:30
@danikdanik danikdanik changed the title fix: declare skills dir in Claude manifest to prevent fixture loading fix(parser): deduplicate component dirs when manifest declares the default Jul 29, 2026
@danikdanik

danikdanik commented Jul 29, 2026

Copy link
Copy Markdown
Author

Thanks @tmchow for the thorough review - you were right on all three points, and I appreciate the time you took to reproduce and measure.

Q1 - you're correct, the recursive-scan claim was wrong. I verified on Claude Code 2.1.220 with compound-engineering@3.20.0 installed (no skills field, all 6 fixture SKILL.md files present in the cache): a fresh claude -p session lists 24 real CE skills and zero fixtures. The loader scans skills/<name>/SKILL.md one level deep, not recursively. The original claim came from skills-auditor sessions that counted SKILL.md files on disk with find (including tests/fixtures/) and assumed those disk-level counts meant the loader was picking them up. I never ran a /skills listing or claude -p to verify. My mistake.

What changed in this PR: I dropped the manifest change entirely. The PR now contains only the parser dedup (Q2) plus the regression test you asked for. Rebased onto current main (1fac044).

Q2 - regression test added. New fixture tests/fixtures/default-dir-declared with a manifest declaring "skills": "./skills/" and two skills (skill-a, skill-b). The test asserts the parser returns 2 skills, not 4. Confirmed it fails on main without the fix (returns 4) and passes with the dedup (returns 2).

Q3 - rebased and re-validated. CLAUDE.md is now the symlink to AGENTS.md from current main. Results:

  • bun run test - 2747 pass, 0 fail
  • bun run plugin:validate - both manifests pass --strict
  • bun run release:validate - in sync, 32 skills

PR title and description updated to match the reframed scope.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bdda198a1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +195 to +197
test("does not double-count skills when the manifest declares the default dir", async () => {
const plugin = await loadClaudePlugin(defaultDirDeclaredRoot)
expect(plugin.skills.map((skill) => skill.name).sort()).toEqual(["skill-a", "skill-b"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add the skills field to the real Claude manifest

This new regression only exercises a temporary fixture with skills: "./skills/"; it never checks the actual .claude-plugin/plugin.json, which still has no skills field in this commit. If the goal is to stop Claude installs of the root plugin from discovering fixture SKILL.md files, users remain on the old manifest behavior until the real Claude manifest declares the skills directory, so please update that manifest or assert it here against compoundPluginRoot.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danikdanik address

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging this; it was correct for the original PR scope, but that scope has since been dropped.

The original PR added "skills": "./skills/" to .claude-plugin/plugin.json to prevent fixture SKILL.md files from loading as real skills. That change (and the recursive-scan claim behind it) was removed after @tmchow's review proved the Claude Code loader scans skills/<name>/SKILL.md one level deep regardless of whether the skills field is present, so fixture SKILL.md files under tests/fixtures/ are never discovered. I verified this on Claude Code 2.1.220 with compound-engineering@3.20.0 installed (no skills field; all 6 fixture SKILL.md files present in the Claude Code plugin cache): a fresh claude -p session listed 24 real CE skills and zero fixtures.

The current PR is purely the parser dedup: when a manifest does declare the default dir explicitly, resolveComponentDirs no longer scans it twice. The regression test exercises that via a fixture because the real .claude-plugin/plugin.json intentionally has no skills field and doesn't need one. A contract test asserting the real manifest omits skills isn't applicable here: that absence is an intentional manifest design choice (rely on default loading), not a parser behavior, and the loader's one-level scan makes it irrelevant to skill discovery either way.

@danikdanik

Copy link
Copy Markdown
Author

This finding is based on the original PR scope, which has been dropped. The manifest change ("skills": "./skills/") was removed after @tmchow's review proved the recursive-scan claim was wrong — the Claude Code loader scans skills/<name>/SKILL.md one level deep regardless of whether the skills field is present, so fixture SKILL.md files under tests/fixtures/ are never discovered.

The PR is now purely the parser dedup fix: when a manifest does declare the default dir explicitly, resolveComponentDirs no longer scans it twice. The regression test exercises that via a fixture because the real .claude-plugin/plugin.json intentionally has no skills field (and doesn't need one). There's nothing to assert against the real manifest because the fixture-loading problem the comment references doesn't exist.

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.

2 participants