Skip to content

fix(scanner): reject duplicate invocation identities without namespace false positives - #1157

Draft
seonghobae wants to merge 9 commits into
feat/claude-plugin-vendored-scope-1099from
feat/claude-plugin-conflicting-identity-1099
Draft

seonghobae wants to merge 9 commits into
feat/claude-plugin-vendored-scope-1099from
feat/claude-plugin-conflicting-identity-1099

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Current repair boundary — 2026-09-12

Successor of #1156 / issue #1099. Keep Draft. This lane owns identity-collision admission only; #1156 retains vendored/generated scope and #1155 retains non-NFC identity handling.

Authoritative identity model

Current Claude Code documentation makes three boundaries relevant here:

  • plugin.json.name is the plugin namespace;
  • plugin Skills and legacy commands/ both expose Skill/slash-command invocation surfaces;
  • legacy Command identity is derived from its path under commands/; command Markdown does not support frontmatter name. Nested directories are part of the command name (for example commands/frontend/component.md → frontend:component).
  • custom agents/ remain a separate surface.

Primary sources:

This is an interoperability/admission precision boundary, not a demonstrated RCE vulnerability.

TDD lineage

d4d5df567963cd9d11f43c37e436263d07471bf7 added the first realistic RED:

  • Skill ship + commands/ship.md must collide;
  • commands/one.md and commands/two.md must stay distinct even if both contain unsupported name: ship frontmatter;
  • plugin namespace may equal the local Skill name;
  • Skill and Agent local names stay separate.

Intervening ordinary descendant 1b761e04f81f6b28eb9e541ce14802af6b5cc158 correctly adopted that finding: it stopped reading Command frontmatter names and put legacy Commands into the same local skill collision set. That delta is retained.

Exact test-only head 4207f11b2bab1d6e1ea9357b263a483e34af3043 adds the remaining nested-path RED. Production used only path.stem, so commands/frontend/deploy.md and commands/backend/deploy.md both collapsed to deploy. Exact GREEN head 62d91cdc096f2a8174ad97ca41042bf11052a132 derives the entire path relative to commands/, removes .md, and joins every segment with :. It is one normal commit ahead / zero behind the RED and changes only five production lines.

Fresh exact-tree focused verification is 16/16 passed, py_compile passed, and git diff --check is clean. This local checkout is exact for the named source/test blobs, but it is not the full repository/coverage/hosted suite. This custom-base stack currently has no PR workflow check-runs; no hosted GREEN or release is claimed.

Minimal GREEN acceptance

  1. Maintain one effective local skill-command invocation collision set for plugin Skills and legacy Commands.
  2. Plugin Skill effective segment follows the supported Skill naming contract: valid frontmatter name where supported, otherwise documented directory/root fallback.
  3. Legacy Command effective segment is its path relative to commands/, .md suffix removed, with every directory segment preserved using Claude's documented : separator; unsupported Command frontmatter name does not redefine identity.
  4. Marketplace/plugin namespace identifiers remain their own domain; plugin namespace text may equal the local segment because the runtime command is namespaced.
  5. Custom Agents remain separate unless independent authoritative evidence demonstrates an actual collision.
  6. Preserve feat(scanner): reject non-NFC plugin identity names #1155 NFC handling, feat(scanner): flag ambiguous vendored plugin code scope #1156 vendored-scope ownership, bounded snippets, receipt/SARIF schema, and secret/bidi non-disclosure.
  7. Run focused tests plus detector 100% statement coverage on one unchanged GREEN head, then applicable Security/SAST/CodeQL and independent current-head review.

Stack state

#1158 was ordinary-restacked through two-parent merge 29d4214c3d634ac7e5af271d503d39b4c7525691. It is now three ahead / zero behind this exact parent, mergeable, and preserves its seven-file secret-to-prompt delta plus every current #1157 source/test change. Combined focused identity and secret-flow verification is 34/34 passed with py_compile and diff checks clean. Both PRs remain Draft pending hosted exact-head gates and independent review. No destructive rebase or force push.

No self-approval, gate weakening, scanner suppression, source-neutral retrigger, or predecessor evidence transfer.

RED contract for duplicate skill, command, marketplace, and
plugin/skill identities. Relates to #1099.
Fail closed when plugin, skill, or command NFC names collide.
Non-NFC names stay the normalized-name class. Relates to #1099.
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

seonghobae added a commit that referenced this pull request Sep 7, 2026
Refresh the single-writer gap baseline from live exact-head evidence:
Draft #1157 on #1156. Relates to #1099.
@seonghobae seonghobae changed the title feat(scanner): reject conflicting plugin identity names fix(scanner): reject duplicate invocation identities without namespace false positives Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Stacked successor: Draft #1158 (feat/claude-plugin-secret-to-prompt-1099 @ 8c9f2d6) on this head.

Unique delta is claude-plugin-secret-to-prompt (named secrets copied into prompts, logs, or child env=). Curl/wget/fetch stays #1137 claude-plugin-secret-to-network. This PR's conflicting-identity unique delta is unchanged. Does not Close #1099 or #1157.

seonghobae added a commit that referenced this pull request Sep 7, 2026
Summary:
- Snapshot 18:16 UTC records Draft #1158 `8c9f2d6` stacked on #1157.
- Named env secrets copied into prompts, logs, or child env fail closed.
- Curl/wget stays the secret-to-network class.

Rationale:
- #999 is the single writer of the product-technical gap baseline.
- #1099 remaining surfaces stay on stacked successors, not Close.

Tests:
- documentation-only; detector evidence lives on #1158 (1878/1878)
@seonghobae seonghobae added bug Something isn't working priority: medium Normal-priority or P2 work labels Sep 8, 2026 — with ChatGPT Codex Connector

Copy link
Copy Markdown
Contributor Author

Namespace FP repair — exact head ba38201644b9ffa75721e14eb56dc4f4c2c9ef5e

Root cause: _conflicting_identity_hits flattened marketplace plugin names and local skill/command/agent names into one seen: set[str]. Equal NFC strings across distinct invocation namespaces therefore emitted claude-plugin-conflicting-identity and failed admission even though they did not identify the same callable surface.

Causal RED on predecessor 6f0eb445ecc0d48dfafd2c3d2525eb7cc1e462f7, using the production package scanner:

  • plugin helper + skill helper: false conflict;
  • skill helper + agent helper: false conflict;
  • two skills named helper: true conflict retained;
  • result: 2 failed / 1 passed.

Minimum repair is one source file, ordinary descendant ba382016...: retain the NFC name, attach its existing namespace (plugin, skill, command, or agent), and keep one set per namespace. No rule ID, severity, receipt schema, SARIF contract, vendored-scope treatment, normalization class, or path budget changed.

Exact source blob is e2e0a02a1fc64bee595826d6681a665aa6fc16b5; the locally verified file has the identical Git blob. The existing conflicting-identity corpus plus the direct causal probes is GREEN: 17 passed; py_compile succeeds. This was an isolated production-module execution with the unrelated #1036 adapter stubbed because the full repository checkout is not mounted; it is not a hosted/full-suite claim. Ruff/Black are unavailable locally.

Keep Draft. This custom-base head has no hosted pull-request workflow and no independent review. Fresh hosted exact-head checks, unchanged-head review, ordinary parent integration, and release remain required.

Copy link
Copy Markdown
Contributor Author

Current-head follow-up after the concurrent RED refinement

Exact lineage:

  • namespace partition repair: ba38201644b9ffa75721e14eb56dc4f4c2c9ef5e
  • concurrent test-only successor: d4d5df567963cd9d11f43c37e436263d07471bf7 (one ahead / zero behind; source blob unchanged)
  • exact GREEN head: 1b761e04f81f6b28eb9e541ce14802af6b5cc158
  • production blob: eee7787fa5cb4905893ea9b3d4d026239965467f
  • exact test blob: 5881b775f03a110769047cf6268cceb803c129fc

Root cause refined from the current official Claude Code plugin contract: commands/ is the flat-Markdown Skill form, but its invocation identity is the command filename/path, not an unsupported frontmatter name. The prior implementation both missed a Skill/legacy-Command collision and rejected two distinct command paths that happened to repeat frontmatter prose.

RED on the exact predecessor production blob plus exact concurrent test blob: 2 failed / 13 passed:

  1. Skill invocation ship plus commands/ship.md was missed.
  2. commands/one.md and commands/two.md were falsely collided because both carried name: ship.

Minimal GREEN derives command identity from path.stem and places it in the Skill collision namespace. It keeps plugin namespace and Agent identity separate, keeps same-Skill duplicates positive, and changes no rule id, severity, snippet, schema, path budget, dependency, or workflow.

Fresh bounded verification with the exact remote blobs above: 15 passed in 0.10s; py_compile passed. This is the focused test module, not the full repository/coverage/hosted suite. The custom-base PR remains Draft until exact-head hosted Checks, parent integration, independent approval, ordinary protected merge, and release. No predecessor result transfers.

Primary contract: https://code.claude.com/docs/en/plugins (retrieved 2026-09-12).

Copy link
Copy Markdown
Contributor Author

Command-identity semantics correction — exact RED d4d5df567963cd9d11f43c37e436263d07471bf7

Fresh primary-doc review found one remaining false-positive/false-negative pair in the current implementation.

Anthropic's current Claude Code docs say plugin skills and legacy commands/ are both exposed on the Skill command surface, but legacy command files take their command name from the file path; command files do not support frontmatter name. Plugin Skills may use frontmatter name (or the skill directory name), and the plugin namespace prefixes the final command.

Primary sources:

The new test-only descendant d4d5df5... therefore requires both sides of the contract:

  • skills/alpha/SKILL.md with effective name ship + commands/ship.md => one effective plugin skill command identity, so admission must fail as claude-plugin-conflicting-identity;
  • commands/one.md and commands/two.md that both contain unsupported name: ship frontmatter => distinct /plugin:one and /plugin:two identities, so this rule must not fire.

Current source still keeps skill and command in separate collision sets and parses command frontmatter names, so this is a realistic RED against the current implementation. No hosted GREEN is claimed; this custom-base head currently has no PR workflow check-runs.

Minimal production GREEN acceptance:

  1. use one local skill-command invocation collision namespace for plugin Skills and legacy Commands;
  2. Skill effective segment = valid frontmatter name where supported, otherwise documented skill directory/root fallback;
  3. legacy Command effective segment = relative commands/ path with the documented filename/path command naming semantics; ignore unsupported frontmatter name for identity;
  4. plugin namespace remains separate from the local command segment;
  5. custom Agents remain a separate surface unless authoritative runtime evidence shows an actual identity collision;
  6. preserve feat(scanner): reject non-NFC plugin identity names #1155 non-NFC and feat(scanner): flag ambiguous vendored plugin code scope #1156 vendored-scope ownership, one bounded name snippet, receipt/SARIF contract, and secret/bidi non-disclosure;
  7. after source repair, rerun the focused suite + detector 100% statement coverage on one unchanged head, then applicable Security/SAST/CodeQL and independent current-head review.

Stack note: advancing #1157 exposed #1158 as non-mergeable against this live predecessor. #1158 must be ordinary non-force restacked after this semantic repair; do not force-push or discard either branch's valid delta.

Copy link
Copy Markdown
Contributor Author

Nested legacy-command identity RED — exact head 4207f11b2bab1d6e1ea9357b263a483e34af3043

Intervening descendant 1b761e04f81f6b28eb9e541ce14802af6b5cc158 correctly repaired the first command-identity defect by deriving legacy command identity from the path and placing Commands on the same skill collision surface. I adopted that delta rather than treating it as a race.

One remaining documented path rule is still missing: nested command directories are part of the slash-command identity. Claude documents .claude/commands/frontend/component.md as /frontend:component, not merely /component.

Current source uses only path.stem, so commands/frontend/deploy.md and commands/backend/deploy.md both collapse to deploy and falsely emit claude-plugin-conflicting-identity.

4207f11... adds the focused realistic RED: those two nested Commands must remain distinct and the package receipt must pass this rule.

Minimal causal GREEN: derive the legacy-command local identity from the path relative to commands/, drop the .md suffix, and preserve every directory segment using Claude's documented : separator (frontend/deploy.md → frontend:deploy). Keep that effective value in the existing shared skill invocation collision set. Do not re-enable command frontmatter name, do not flatten nested paths, and preserve the namespace/agent/NFC/vendored/receipt/SARIF contracts already fixed in this stack.

No hosted GREEN is claimed for this custom-base head.

Copy link
Copy Markdown
Contributor Author

Current exact GREEN for the nested legacy-Command boundary

  • RED head: 4207f11b2bab1d6e1ea9357b263a483e34af3043
  • GREEN head: 62d91cdc096f2a8174ad97ca41042bf11052a132 (one ahead / zero behind)
  • production blob: 845f9be2121d50856593a8b2bcc3dbbc15014921
  • exact test blob: cac28ecf706453cf7097234abc6ffaad806c2b47
  • effective source delta: 3 additions / 2 deletions

Exact RED checkout produced 1 failed / 15 passed: commands/frontend/deploy.md and commands/backend/deploy.md were both collapsed to deploy by path.stem.

The minimal repair derives the full path relative to root commands/, removes the Markdown suffix, and joins every directory segment with :. That preserves the documented frontend:deploy and backend:deploy identities while retaining the previous Skill↔legacy-Command collision and unsupported-frontmatter negative boundaries.

Fresh exact source/test verification: 16 passed in 0.18s; py_compile and git diff --check passed. This is focused local evidence, not full-suite/coverage/hosted evidence. No PR workflow runs or formal reviews exist for this custom-base head, so Draft remains correct.

Primary contract: https://code.claude.com/docs/en/plugins and https://code.claude.com/docs/en/skills (retrieved 2026-09-12).

Copy link
Copy Markdown
Contributor Author

Fresh exact-tree regression expansion: remote head 62d91cdc096f2a8174ad97ca41042bf11052a132 and the verified checkout have identical tree b873cf90162c021ae4cc40d755aae5640cb9aa2b. PYTHONPATH=. python -m pytest -q tests/test_claude_plugin*.py completed 202 passed in 2.26s. The previously reported focused 16/16, py_compile, and diff check remain bounded subsets. This is the complete Claude-plugin test family at this tree, not the repository-wide suite, coverage gate, hosted Checks, formal approval, protected merge, or release; Draft remains correct.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority: medium Normal-priority or P2 work

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant