You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Skill duplicate roots can change available_skills across restarts #32202
When the same skill name exists in more than one discovered skill root, OpenCode can pick a different root across process starts.
The final <available_skills> block is sorted by skill name, but duplicate resolution happens earlier while loading matching SKILL.md files. Duplicate names currently use last-writer-wins, and the loader processes matches concurrently, so the winning location can depend on file I/O completion order.
This makes the system prompt bytes change after a restart even when no skill files changed. For users with prompt caching, that causes an unnecessary one-time cache miss on the first turn after restart.
Plugins
N/A
OpenCode version
Current dev
Steps to reproduce
Create the same skill name in two discovered roots, for example:
.claude/skills/example/SKILL.md
.opencode/skills/example/SKILL.md
Start OpenCode and inspect the verbose <available_skills> system prompt entry for example.
Restart OpenCode and inspect the same entry again.
The selected <location> should be stable according to discovery precedence. Instead, duplicate winners can vary because skill files are loaded concurrently and duplicate names overwrite each other as each load completes.
This issue might be a duplicate of existing issues. Please check:
Skill enumeration is non-deterministic when the same skill is reachable through multiple discovery roots #29950: Describes the exact same root cause — non-deterministic skill location resolution when the same skill name is reachable through multiple discovery roots (e.g. .claude/skills/ and .agents/skills/), causing per-session system prompt byte changes and breaking prompt cache hit rates. It also proposes the same fix (deduplicate by basename at discovery time with a canonical root preference).
If your issue adds new context or a different angle not covered there, please note that in a comment on #29950 or here.
We hit this same root cause in headless opencode run (1.17.18) and can add a measured cost consequence: with the same skills installed under two discovery roots (~/.claude/skills and ~/.agents/skills), the surviving <location> line flips per process — and since Anthropic prompt caching requires a byte-identical prefix, one flipped line is a whole-prefix cache miss.
The failure is effectively invisible: token counts still look plausible (cache writes present, reads ~0), and nothing errors. We only caught it auditing tokens.cache.read in opencode.db across repeated 2-turn run/--continue conversations — turn-2 cache.read was intermittently 0 when the rendered location flipped between processes. At Anthropic pricing the miss is roughly a 12× per-turn input overpay (cache read 0.1× vs full input re-write).
Workaround we use meanwhile: "permission": {"skill": "deny"} removes the <available_skills> block entirely (also saves ~2.6k tokens/call).
+1 for deterministic root precedence or de-dup by skill id — identical installs should render identical prompts.
Researched and written with AI assistance; measurements are real but verify before relying on them.
Confirming @awswanson's measurement, with one correction.
openrouter/anthropic/claude-haiku-4.5, ~21k-token system prompt, dev from source, two-prompt sessions, #39008 applied so skill precedence is the only variable:
Consistent with the ~12× overpay above. The correction: not intermittent here — 6 of 6 turns missed, and the write count differed on every request (21348, 21360, 21369, 21381), so the prompt drifts continuously rather than only per process.
On the two closed patches, since someone will need to pick one up: #32203 also sorts Glob.scan output and dirs, and the load race is only one of three ordering surfaces. It applies cleanly to current dev, 25 skill tests pass. #30184 adds a three-mode DEDUPE_MODE and inverts precedence so .claude beats .agents. Neither was closed on review — both matched the automated inactivity cleanup.
Additional Windows evidence from a large physical-copy installation (not symlinks):
OpenCode 1.14.48
~/.claude/skills: 351 SKILL.md files
~/.agents/skills: 398 SKILL.md files
342 identical relative paths across both roots
28 repeated nested skill-name/skill-name/SKILL.md layouts in each root
one startup log emitted 394 duplicate skill name warnings and ended with 331 unique skills
neither root is a symlink; matching paths are separate physical files
This supports the issue's two underlying concerns beyond the symlink case: both global roots are scanned, and duplicate-name resolution needs deterministic source precedence rather than concurrent last-writer behavior. A bounded-warning policy would also keep real diagnostics visible at this scale.
I have not removed either tree because they serve different clients; a safe local migration needs one declared canonical owner plus client adapters. Upstream realpath/content dedup and deterministic root precedence would avoid forcing users to break another client's native discovery.
Hi, I checked the current dev branch. add() still overwrites state.skills[name], and loadSkills() still parses matches with unbounded concurrency, so the winning source can depend on completion order.
I reviewed #32203 and #30184 as well. The cleanup bot closed both after inactivity; maintainers did not reject either patch. I found no open PR for #32202.
I can revive the smaller #32203 approach against current dev: sort scan results and directories, resolve duplicates in discovery order, preserve the current source precedence, and add Windows coverage for .claude, .agents, and .opencode. I would leave any new precedence policy for a separate discussion.
Is someone working on this, or may I send that PR?
Related: #46327 — same multi-root collision, but the user-visible failure on 1.18.25 Desktop is a WARN flood (once per workspace) when ~/.claude/skills/<name> is a symlink to ~/.agents/skills/<name> (same inode, still two locations). Nested remotion-best-practices/**/SKILL.md collides with top-level copies as well.
Independent reproduction on 1.18.15 (released build, macOS 26.6.2 arm64), with a datapoint the thread does not have yet: this reproduces even when the two roots are the same directory.
I consolidated my hubs down to one, expecting that to fix it:
There is now exactly one real skills directory on disk. It still fails, because discovery globs .claude and .agents independently and the glob follows the symlinked root, so both phases still yield distinct absolute path strings for the same file.
Minimal fixture — two roots, one target, isolated HOME:
WARN duplicate skill name name=one existing=.../.agents/skills/one/SKILL.md duplicate=.../.claude/skills/one/SKILL.md
WARN duplicate skill name name=two existing=.../.claude/skills/two/SKILL.md duplicate=.../.agents/skills/two/SKILL.md
Note one and two resolve from opposite roots within a single run.
Confirming @sergical's correction over "per process": on my real install (13 skills, ~28.3k-char system prompt, anthropic/claude-haiku-4-5 via a LiteLLM gateway, captured on the wire by a proxy), 6 consecutive opencode run invocations gave:
distinct system-prompt hashes: 6 / 6
cache hits: 0 / 6 (write=65490 every run, read=0 every run)
sysLen was identical (28351) in all six, so this is not content drift — only the <location> strings move. The .claude-vs-.agents split wandered per run: claude=3,4,2,2,6,4 of 20 location lines. Deterministic ordering alone would fix it; byte-length stability means the prompt is otherwise stable.
Two practical notes for anyone landing a fix:
skills.paths cannot be used as a workaround. It is additive (skills?.paths ?? [] is iterated after the built-in roots), so it cannot suppress a hardcoded root. The roots are constants: var bA=".claude", xA=".agents", GA="skills/**/SKILL.md".
The only configuration I found that yields 0 duplicates is ~/.claude/skills not existing — which is not viable for anyone also running Claude Code, since it has no configurable skills location.
Dedupe by resolved realpath (rather than path string) would additionally cover the symlinked-root case above, which basename dedupe alone handles only incidentally.
Both #32203 and #30184 appear to have been closed by the inactivity bot rather than on review.
Posted by an AI agent on behalf of @cmsflash, who ran into this on his own machine. The investigation and all measurements above were performed by the agent against a real 1.18.15 install and are reproducible.
Independent reproduction on 1.18.20 (release build, macOS 26 arm64), with two datapoints the thread may find useful: a measurement of what the flip costs on a local prefix-cached backend, and a workaround that made the prompt byte-stable.
Setup: ~20 skills present as physical copies in both ~/.claude/skills and ~/.agents/skills (Claude Code and Codex each read only their own root, so both copies have to exist). Also the superpowers plugin, whose package ships brainstorming and systematic-debugging, which also exist in both roots.
Method: pointed the provider baseURL at a tiny local HTTP server that dumps the request body, ran opencode run --agent <custom> "selam" three times per configuration, diffed the captured system prompts.
configuration
3 consecutive starts byte-identical?
default (both roots + plugin)
no – every <location> flips between .claude and .agents
~/.agents/skills symlinked to ~/.claude/skills
no – same flips, path text differs
only the 2 plugin-duplicated skills removed from ~/.agents
no – remaining 18 still flip; the 2 now flip between ~/.claude and the plugin package
OPENCODE_DISABLE_EXTERNAL_SKILLS=1 + ~/.config/opencode/skills = symlinks to ~/.claude/skills/*
no – the 2 plugin-duplicated skills still flip between the single root and the package
same, but the 2 plugin-provided skills excluded from the single root
yes
So the race is not limited to the ~/.claude vs ~/.agents pair; a plugin package that ships a skill of the same name races with a single configured root too.
Cost, measured on a local backend with a persistent prefix cache (oMLX, 2048-token blocks): the first divergent <location> lands around token ~6.1–8k of a 13.4k-token prompt, so a fresh session reuses only the first 3 blocks (tool definitions) and re-prefills the remaining ~7k tokens every start. On a 27B model on an M5 laptop that is ~30 s of extra prefill per new session that would otherwise be a cache hit. In-session turns are unaffected. For cloud providers this is the "one cache miss per restart" already described above, but for slow local backends it is a noticeable wall-clock penalty.
Workaround for anyone needing the exact bytes stable today: OPENCODE_DISABLE_EXTERNAL_SKILLS=1, a single ~/.config/opencode/skills root built from per-skill symlinks, and leaving out any skill that a plugin already provides. Deterministic precedence in add() (or sorting the scan results before dedup, as #32203 proposed) would make this unnecessary.
In OpenCode v2, the winner among skills with the same name is deterministic. Sources load in a fixed order (built-in, then .claude/skills, then .agents/skills, then configured skill paths), later sources win, and files are read in sorted order. <available_skills> no longer changes between restarts.
Fixed in #41622; the precedence is documented in #41844.
Closing as completed. If something doesn't work on v2, please open a new issue.
Activity
github-actions commentedon Jun 13, 2026
This issue might be a duplicate of existing issues. Please check:
.claude/skills/and.agents/skills/), causing per-session system prompt byte changes and breaking prompt cache hit rates. It also proposes the same fix (deduplicate by basename at discovery time with a canonical root preference).If your issue adds new context or a different angle not covered there, please note that in a comment on #29950 or here.
awswanson commentedon Jul 13, 2026
We hit this same root cause in headless
opencode run(1.17.18) and can add a measured cost consequence: with the same skills installed under two discovery roots (~/.claude/skillsand~/.agents/skills), the surviving<location>line flips per process — and since Anthropic prompt caching requires a byte-identical prefix, one flipped line is a whole-prefix cache miss.The failure is effectively invisible: token counts still look plausible (cache writes present, reads ~0), and nothing errors. We only caught it auditing
tokens.cache.readin opencode.db across repeated 2-turnrun/--continueconversations — turn-2cache.readwas intermittently 0 when the rendered location flipped between processes. At Anthropic pricing the miss is roughly a 12× per-turn input overpay (cache read 0.1× vs full input re-write).Workaround we use meanwhile:
"permission": {"skill": "deny"}removes the<available_skills>block entirely (also saves ~2.6k tokens/call).+1 for deterministic root precedence or de-dup by skill id — identical installs should render identical prompts.
sergical commentedon Jul 27, 2026
Researched and written with AI assistance; measurements are real but verify before relying on them.
Confirming @awswanson's measurement, with one correction.
openrouter/anthropic/claude-haiku-4.5, ~21k-token system prompt,devfrom source, two-prompt sessions, #39008 applied so skill precedence is the only variable:Consistent with the ~12× overpay above. The correction: not intermittent here — 6 of 6 turns missed, and the write count differed on every request (21348, 21360, 21369, 21381), so the prompt drifts continuously rather than only per process.
On the two closed patches, since someone will need to pick one up: #32203 also sorts
Glob.scanoutput anddirs, and the load race is only one of three ordering surfaces. It applies cleanly to currentdev, 25 skill tests pass. #30184 adds a three-modeDEDUPE_MODEand inverts precedence so.claudebeats.agents. Neither was closed on review — both matched the automated inactivity cleanup.frankxai commentedon Aug 6, 2026
Additional Windows evidence from a large physical-copy installation (not symlinks):
~/.claude/skills: 351SKILL.mdfiles~/.agents/skills: 398SKILL.mdfilesskill-name/skill-name/SKILL.mdlayouts in each rootduplicate skill namewarnings and ended with 331 unique skillsThis supports the issue's two underlying concerns beyond the symlink case: both global roots are scanned, and duplicate-name resolution needs deterministic source precedence rather than concurrent last-writer behavior. A bounded-warning policy would also keep real diagnostics visible at this scale.
I have not removed either tree because they serve different clients; a safe local migration needs one declared canonical owner plus client adapters. Upstream realpath/content dedup and deterministic root precedence would avoid forcing users to break another client's native discovery.
y10reo commentedon Aug 11, 2026
Hi, I checked the current
devbranch.add()still overwritesstate.skills[name], andloadSkills()still parses matches with unbounded concurrency, so the winning source can depend on completion order.I reviewed #32203 and #30184 as well. The cleanup bot closed both after inactivity; maintainers did not reject either patch. I found no open PR for #32202.
I can revive the smaller #32203 approach against current
dev: sort scan results and directories, resolve duplicates in discovery order, preserve the current source precedence, and add Windows coverage for.claude,.agents, and.opencode. I would leave any new precedence policy for a separate discussion.Is someone working on this, or may I send that PR?
eshe-huli commentedon Aug 31, 2026
Related: #46327 — same multi-root collision, but the user-visible failure on 1.18.25 Desktop is a WARN flood (once per workspace) when
~/.claude/skills/<name>is a symlink to~/.agents/skills/<name>(same inode, still two locations). Nestedremotion-best-practices/**/SKILL.mdcollides with top-level copies as well.cmsflash commentedon Sep 1, 2026
Independent reproduction on 1.18.15 (released build, macOS 26.6.2 arm64), with a datapoint the thread does not have yet: this reproduces even when the two roots are the same directory.
I consolidated my hubs down to one, expecting that to fix it:
There is now exactly one real skills directory on disk. It still fails, because discovery globs
.claudeand.agentsindependently and the glob follows the symlinked root, so both phases still yield distinct absolute path strings for the same file.Minimal fixture — two roots, one target, isolated
HOME:Note
oneandtworesolve from opposite roots within a single run.Confirming @sergical's correction over "per process": on my real install (13 skills, ~28.3k-char system prompt,
anthropic/claude-haiku-4-5via a LiteLLM gateway, captured on the wire by a proxy), 6 consecutiveopencode runinvocations gave:sysLenwas identical (28351) in all six, so this is not content drift — only the<location>strings move. The.claude-vs-.agentssplit wandered per run:claude=3,4,2,2,6,4of 20 location lines. Deterministic ordering alone would fix it; byte-length stability means the prompt is otherwise stable.Two practical notes for anyone landing a fix:
skills.pathscannot be used as a workaround. It is additive (skills?.paths ?? []is iterated after the built-in roots), so it cannot suppress a hardcoded root. The roots are constants:var bA=".claude", xA=".agents", GA="skills/**/SKILL.md".~/.claude/skillsnot existing — which is not viable for anyone also running Claude Code, since it has no configurable skills location.Dedupe by resolved realpath (rather than path string) would additionally cover the symlinked-root case above, which basename dedupe alone handles only incidentally.
Both #32203 and #30184 appear to have been closed by the inactivity bot rather than on review.
Posted by an AI agent on behalf of @cmsflash, who ran into this on his own machine. The investigation and all measurements above were performed by the agent against a real 1.18.15 install and are reproducible.
afsozer commentedon Sep 6, 2026
Independent reproduction on 1.18.20 (release build, macOS 26 arm64), with two datapoints the thread may find useful: a measurement of what the flip costs on a local prefix-cached backend, and a workaround that made the prompt byte-stable.
Setup: ~20 skills present as physical copies in both
~/.claude/skillsand~/.agents/skills(Claude Code and Codex each read only their own root, so both copies have to exist). Also thesuperpowersplugin, whose package shipsbrainstormingandsystematic-debugging, which also exist in both roots.Method: pointed the provider
baseURLat a tiny local HTTP server that dumps the request body, ranopencode run --agent <custom> "selam"three times per configuration, diffed the captured system prompts.<location>flips between.claudeand.agents~/.agents/skillssymlinked to~/.claude/skills~/.agents~/.claudeand the plugin packageOPENCODE_DISABLE_EXTERNAL_SKILLS=1+~/.config/opencode/skills= symlinks to~/.claude/skills/*So the race is not limited to the
~/.claudevs~/.agentspair; a plugin package that ships a skill of the same name races with a single configured root too.Cost, measured on a local backend with a persistent prefix cache (oMLX, 2048-token blocks): the first divergent
<location>lands around token ~6.1–8k of a 13.4k-token prompt, so a fresh session reuses only the first 3 blocks (tool definitions) and re-prefills the remaining ~7k tokens every start. On a 27B model on an M5 laptop that is ~30 s of extra prefill per new session that would otherwise be a cache hit. In-session turns are unaffected. For cloud providers this is the "one cache miss per restart" already described above, but for slow local backends it is a noticeable wall-clock penalty.Workaround for anyone needing the exact bytes stable today:
OPENCODE_DISABLE_EXTERNAL_SKILLS=1, a single~/.config/opencode/skillsroot built from per-skill symlinks, and leaving out any skill that a plugin already provides. Deterministic precedence inadd()(or sorting the scan results before dedup, as #32203 proposed) would make this unnecessary.neriousy commentedon Oct 1, 2026
In OpenCode v2, the winner among skills with the same name is deterministic. Sources load in a fixed order (built-in, then
.claude/skills, then.agents/skills, then configured skill paths), later sources win, and files are read in sorted order.<available_skills>no longer changes between restarts.Fixed in #41622; the precedence is documented in #41844.
Closing as completed. If something doesn't work on v2, please open a new issue.