Skip to content

Permission scanner does not deduplicate symlinked skill directories, doubling ruleset size and log volume #17219

@XucroYuri

Description

@XucroYuri

Bug Summary

OpenCode's permission system scans multiple skill directories (~/.claude/skills/, ~/.agents/skills/, ~/.config/opencode/skills/) and registers external_directory permission rules for each discovered skill. However, it does not deduplicate symlinks — if ~/.claude/skills/foo is a symlink to ~/.agents/skills/foo, OpenCode registers two separate rules for the same physical directory, doubling the permission ruleset size with zero security or functional benefit.

Combined with the fact that the full ruleset is serialized into every service=permission log line at INFO level (see #17218), this symlink duplication directly doubles the log bloat rate — contributing to 50GB+ log accumulation in hours.

Environment

Component Version
OpenCode 1.1.53
OS macOS 15 (darwin-arm64)
Skills in ~/.agents/skills/ 613 (actual files)
Skills in ~/.claude/skills/ 613 (all symlinks → ~/.agents/skills/)
Skills in ~/.config/opencode/skills/ 10

Reproduction

Setup

# Install skills via any skill manager — they land in ~/.agents/skills/
# The skill installer also creates symlinks in ~/.claude/skills/
ls -la ~/.claude/skills/ | head -5
# Output:
# lrwxr-xr-x  accessibility-compliance -> ../../.agents/skills/accessibility-compliance
# lrwxr-xr-x  add-educational-comments -> ../../.agents/skills/add-educational-comments
# ...

# Verify same inode (same physical file):
stat -f "%i" ~/.agents/skills/accessibility-compliance/SKILL.md
# 110667057
stat -f "%i" ~/.claude/skills/accessibility-compliance/SKILL.md
# 110667057  ← identical

Steps

  1. Have skills in both ~/.agents/skills/ and ~/.claude/skills/ (symlinked)
  2. Start OpenCode
  3. Trigger any tool call
  4. Inspect the permission log line

Expected

OpenCode resolves symlinks and deduplicates, registering one external_directory rule per unique physical skill directory:

ruleset=[
  {"permission":"external_directory","pattern":"/Users/xuyu/.agents/skills/foo/*","action":"allow"},
  ...
]
# Total: ~660 external_directory rules for 613 skills + other paths

Actual

OpenCode registers two rules per skill — one for each scanned path, regardless of symlink resolution:

ruleset=[
  {"permission":"external_directory","pattern":"/Users/xuyu/.agents/skills/foo/*","action":"allow"},
  {"permission":"external_directory","pattern":"/Users/xuyu/.claude/skills/foo/*","action":"allow"},
  ...
]
# Total: 1,262 external_directory rules for 613 unique skills (2x duplication)

Impact

Quantified

Metric With symlink duplication Without (deduplicated)
Permission rules 1,278 660
Ruleset size per log line ~148KB ~76KB
Log bytes per tool call (2 lines) ~296KB ~152KB
Log growth rate (active session) ~29MB/min ~15MB/min

The duplication alone accounts for ~50% of the total log volume — turning a bad situation (full-ruleset logging, #17218) into a catastrophic one.

Why this happens

The skill ecosystem uses a common pattern:

  • Primary store: ~/.agents/skills/ — actual SKILL.md files
  • Compatibility layer: ~/.claude/skills/ — symlinks to primary store (for Claude Code compatibility)

The skill installer (tracked via ~/.agents/.skill-lock.json) creates both, and OpenCode's permission scanner treats them as independent directories.

Proposed Fix

Option A: Resolve symlinks before rule registration (Recommended)

Before registering an external_directory rule, call fs.realpathSync() (or equivalent) on the skill directory. If the resolved path already has a rule, skip the duplicate.

// Pseudocode
const resolvedPaths = new Set<string>();
for (const skillDir of allSkillDirectories) {
  const realPath = fs.realpathSync(skillDir);
  if (resolvedPaths.has(realPath)) continue; // skip symlink duplicate
  resolvedPaths.add(realPath);
  rules.push({ permission: "external_directory", pattern: `${skillDir}/*`, action: "allow" });
}

Option B: Deduplicate by skill name

If two directories contain a skill with the same name, only register one rule (prefer the primary/non-symlink path).

Option C: Register a single wildcard rule per skill root

Instead of one rule per skill, register one rule per skill directory root:

{"permission":"external_directory","pattern":"/Users/xuyu/.agents/skills/*","action":"allow"}

This would reduce 618 rules to 1 rule — a 618x reduction. The security granularity loss is minimal since all skills in the directory are already trusted.

Workaround

Remove the symlink directory and recreate it empty:

rm -rf ~/.claude/skills
mkdir -p ~/.claude/skills
# Reduces permission rules from 1,278 to 660 immediately
# Takes effect on next OpenCode session start

Related

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)perfIndicates a performance issue or need for optimization

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions