[Bug]: Same-name skills from different sources collide and can invoke the wrong skill#11161

openedSep 11, 2026byFfloklein

Problem

Two unrelated skills named code-review collide in T3 Code: Matt Pocock's locally installed skill (~/.agents/skills/code-review/SKILL.md) and an official plugin's skill with the same name. Selecting code-review invoked the plugin skill instead of Matt's. We worked around it by renaming Matt's skill to standards-and-spec-review.

The same happens with one skill copied into two roots, such as ~/.codex/skills and ~/.agents/skills.

Reproduction

  1. Install two enabled skills with the same name but different files and instructions.
  2. Open the Codex $ skill picker (or / with skills shown).
  3. Try to pick the second skill.

Expected

Each skill file is its own row in the picker, and the skill you pick is the one that runs.

Actual

Still reproducible on main at cd41c4ada0, after the provider V2 rewrite:

  • packages/client-runtime/src/providerSkills.ts:33: dedupeProviderSkillsByName keeps only the first skill per name, so the second file has no row.
  • apps/web/src/components/chat/ChatComposer.tsx:4015 and apps/mobile/src/features/threads/use-composer-command-menu.ts:160: a pick inserts a bare $name.
  • apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts:3005: the prompt reaches Codex as plain text.

Root cause

Codex skips a plain $name when more than one enabled skill has that name (collect_explicit_skill_mentions in codex-rs/skills/src/selection.rs requires a name count of 1). The selected skill is never injected, and the model guesses which SKILL.md to read.

Codex binds a linked mention, [$name](/path/to/SKILL.md), to that exact file. Checked with codex-cli 0.160.1 and two dup skills in nested .agents/skills roots, each telling the model to answer with a different word:

PromptResult
[$dup](…/sub/.agents/skills/dup/SKILL.md) goWATERMELON (sub copy, injected)
[$dup](…/.agents/skills/dup/SKILL.md) goPINEAPPLE (root copy, injected)
$dup gonothing injected; the model ran Get-Content on one SKILL.md it chose

Provider scope

This applies to every supported harness, not only Codex:

  • T3's Claude, Cursor, Grok and Antigravity discovery keeps only the first skill per name, so a second file with the same name cannot be picked.
  • Every provider invokes skills by name, so even a listed second file would not run. Claude Code, for example, runs the user copy of /name even when the project has its own.

The fix must list each file and make each harness run the picked file, not the one its name resolution prefers.

  • #8043 collapsed same-name rows to fix duplicate menu IDs. That assumed same-name skills are interchangeable. Menu IDs should use the file path instead.
  • #6095 is the broader request for structured Codex skill input.
  • #11166 was a first fix, closed during the provider V2 rewrite.

Triage

Confirmed on current main (c52b8d96e4b34201f19b5e5bb12c6b2a77bfaa9a). Distinct skills that share a name but come from different sources are not independently selectable, and the selected source/path is not preserved through dispatch.

This is not a hardcoded preference for official plugins. First matching name wins, so [plugin, personal] keeps the plugin and [personal, plugin] keeps the personal skill — matching the reporter’s Node check.

What the code does today

  1. Picker collapses by name. dedupeProviderSkillsByName in packages/client-runtime/src/providerSkills.ts keeps the first trimmed, case-insensitive name and ignores path / scope. The same helper feeds the web $ search (apps/web/src/providerSkillSearch.ts), the slash-menu skill list, and the mobile composer menu.

  2. Selection is name-only. Web (ChatComposer.tsx) and mobile insert $${item.skill.name}. Menu row IDs are skill:${provider}:${skill.name}, which is why #8043 collapsed duplicates to restore unique highlight/scroll targets.

  3. Send does not bind a path. Codex buildTurnStartParams (CodexSessionRuntime.ts) sends the prompt as { type: "text" } only. There is no structured { type: "skill", name, path } input. Claude dispatch (ClaudeSkillDispatch.ts) is also name-set based.

Source badges already exist via resolveProviderSkillSourceKind, and Codex skills/list already returns path + scope (kept on ServerProviderSkill). The identity is dropped in the picker and never rebound on send.

  • #8043 (merged) fixed duplicate menu IDs by assuming same-name definitions are interchangeable. That holds for the same skill mirrored in .codex and .agents; it does not hold for unrelated personal vs plugin skills that happen to share a name.
  • #6095 is the broader “explicit $skill is not bound as a Codex skill input” gap. Closed PRs #7196 / #9290 tried send-time SkillUserInput. Even if Codex now loads $name from text (as discussed on #6095), two sources with the same name still cannot be chosen independently.

Suggested fix

  • Deduplicate by path (or name + source), not name alone. Keep true same-path duplicates collapsed so #8043’s highlight/scroll fix stays intact.
  • Give menu rows a stable unique id that includes path.
  • Preserve the selected path through send (Codex structured skill input with name + path; Claude needs an equivalent if same-name collisions exist there).
  • Keep source badges so the two rows are distinguishable.

Workaround until then: rename one skill (as the reporter did: standards-and-spec-review).

Type: bug (medium). Action: keep open; accepted for a path-aware identity + dispatch fix.

Already covered by the open maintainer-taken-over fix #19702. It preserves separate same-name files in discovery and the pickers, carries the selected path through drafts, and binds provider dispatch to that file. Its verification includes live Codex and Claude runs with distinct markers for both files. No duplicate PR opened; this issue remains open until that work lands.