Summary
opencode auto-discovers skills from six default locations including ~/.claude/skills/ and ~/.agents/skills/. When the same skill basename is reachable through more than one root (a common Claude Code setup, where ~/.claude/skills/<name> is symlinked to ~/.agents/skills/<name>), opencode's discovery randomly assigns one path or the other per session in the emitted <skill><location>...</location></skill> entries. This injects per-session volatility into the system prompt at random offsets, defeating prefix-cache reuse on every upstream backend.
Environment
- opencode dev branch (also reproduced on 1.15.11 release)
- Skills available in both
~/.claude/skills/ (mostly symlinks → ~/.agents/skills/) and ~/.agents/skills/
- 20 skills are reachable through both roots in our setup
Steps to reproduce
- Have
~/.agents/skills/foo/SKILL.md and ~/.claude/skills/foo -> ../../.agents/skills/foo (symlink, or independent copy)
- Open two fresh opencode sessions
- Capture the outgoing system prompt in each session (we did this via a LiteLLM
async_pre_call_hook writing the full system payload to disk)
- Diff the two captures
Observed
Two fresh sessions in the same cwd, with identical config, produced byte-different <skill><location>...</location></skill> entries:
@@ -443,7 +443,7 @@
<skill>
<name>linear-reporting</name>
- <location>file:///Users/.../.agents/skills/linear-reporting/SKILL.md</location>
+ <location>file:///Users/.../.claude/skills/linear-reporting/SKILL.md</location>
</skill>
@@ -453,7 +453,7 @@
<skill>
<name>officecli</name>
- <location>file:///Users/.../.agents/skills/officecli/SKILL.md</location>
+ <location>file:///Users/.../.claude/skills/officecli/SKILL.md</location>
</skill>
@@ -495,7 +495,7 @@
<skill>
<name>pr-verify</name>
- <location>file:///Users/.../.claude/skills/pr-verify/SKILL.md</location>
+ <location>file:///Users/.../.agents/skills/pr-verify/SKILL.md</location>
</skill>
Note that the resolved root flips both directions across skills: linear-reporting goes .agents → .claude between sessions, while pr-verify goes .claude → .agents. So this isn't "preferred root" inconsistency — it's purely non-deterministic per-skill resolution.
Why it matters
Each one of these <location> flips changes ~30 characters of the system prompt at a deep offset. Every upstream prefix cache (Anthropic cache_control, OpenAI auto-cache, vLLM APC, oMLX paged SSD) keys on byte/block-level prefix match. Even with the prompt structurally identical, the location-string flips break cache hit rate.
Measured impact in our setup (Qwen3-Coder-Next-80B-A3B via oMLX on M3 Ultra):
- With this non-determinism: cache.read=0 or ~4096 tokens of 30,000 (~13%) per fresh session, ~30 s TTFT every session
- After workaround (see below): cache.read=28,672 / 30,000 (95.6%), 3–5 s TTFT on subsequent fresh sessions
Workaround
Set OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1 in the launching environment. This drops ~/.claude/skills/ from external discovery entirely, leaving ~/.agents/skills/ as the only external root and eliminating the duplicate-resolution path. Tested and confirmed effective.
This workaround is not documented. We discovered it by reading packages/opencode/src/skill/index.ts (the disableClaudeCodeSkills flag) and packages/opencode/src/effect/runtime-flags.ts (the env var wire-up).
Proposed fix
In packages/opencode/src/skill/index.ts, deduplicate skills by basename at discovery time. When the same skill name appears in multiple roots, pick a canonical location (e.g., prefer .agents/skills/<name> over .claude/skills/<name>, or sort roots alphabetically and take the first match). Then sort the final set deterministically before emitting <skill> blocks.
This would:
- Eliminate the per-session non-determinism
- Restore prefix-cache hit rate to the structural maximum
- Make the
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS workaround unnecessary for this scenario
Related
Summary
opencode auto-discovers skills from six default locations including
~/.claude/skills/and~/.agents/skills/. When the same skill basename is reachable through more than one root (a common Claude Code setup, where~/.claude/skills/<name>is symlinked to~/.agents/skills/<name>), opencode's discovery randomly assigns one path or the other per session in the emitted<skill><location>...</location></skill>entries. This injects per-session volatility into the system prompt at random offsets, defeating prefix-cache reuse on every upstream backend.Environment
~/.claude/skills/(mostly symlinks →~/.agents/skills/) and~/.agents/skills/Steps to reproduce
~/.agents/skills/foo/SKILL.mdand~/.claude/skills/foo -> ../../.agents/skills/foo(symlink, or independent copy)async_pre_call_hookwriting the full system payload to disk)Observed
Two fresh sessions in the same cwd, with identical config, produced byte-different
<skill><location>...</location></skill>entries:Note that the resolved root flips both directions across skills:
linear-reportinggoes.agents → .claudebetween sessions, whilepr-verifygoes.claude → .agents. So this isn't "preferred root" inconsistency — it's purely non-deterministic per-skill resolution.Why it matters
Each one of these
<location>flips changes ~30 characters of the system prompt at a deep offset. Every upstream prefix cache (Anthropiccache_control, OpenAI auto-cache, vLLM APC, oMLX paged SSD) keys on byte/block-level prefix match. Even with the prompt structurally identical, the location-string flips break cache hit rate.Measured impact in our setup (Qwen3-Coder-Next-80B-A3B via oMLX on M3 Ultra):
Workaround
Set
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1in the launching environment. This drops~/.claude/skills/from external discovery entirely, leaving~/.agents/skills/as the only external root and eliminating the duplicate-resolution path. Tested and confirmed effective.This workaround is not documented. We discovered it by reading
packages/opencode/src/skill/index.ts(thedisableClaudeCodeSkillsflag) andpackages/opencode/src/effect/runtime-flags.ts(the env var wire-up).Proposed fix
In
packages/opencode/src/skill/index.ts, deduplicate skills by basename at discovery time. When the same skill name appears in multiple roots, pick a canonical location (e.g., prefer.agents/skills/<name>over.claude/skills/<name>, or sort roots alphabetically and take the first match). Then sort the final set deterministically before emitting<skill>blocks.This would:
OPENCODE_DISABLE_CLAUDE_CODE_SKILLSworkaround unnecessary for this scenarioRelated