Skip to content

Skill enumeration is non-deterministic when the same skill is reachable through multiple discovery roots #29950

@rikkarth

Description

@rikkarth

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

  1. Have ~/.agents/skills/foo/SKILL.md and ~/.claude/skills/foo -> ../../.agents/skills/foo (symlink, or independent copy)
  2. Open two fresh opencode sessions
  3. 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)
  4. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions