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.mdfrontend: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.mdfrontend: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.

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