Skip to content

feat(opencode): add skills.exclude for hidden and trash copies - #159

Merged
brentyates-swx merged 6 commits into
swxtchfrom
fm/opencode-skills-exclude-59
Oct 10, 2026
Merged

brentyates-swx merged 6 commits into
swxtchfrom
fm/opencode-skills-exclude-59

Conversation

@brentyates-swx

@brentyates-swx brentyates-swx commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary

Adds skills.exclude: glob patterns for SKILL.md files that skip copies in every skill root. Hidden per-host copies, such as gstack's .openclaw, .factory, and .hermes installs under ~/.claude/skills/gstack, and .trash copies are no longer loaded, so they stop filling the skill listing sent on every first request.

  • Each pattern is matched against the SKILL.md path relative to the root its scan started from, so **/.trash/** works the same in every root: global ~/.claude and ~/.agents, project ancestors, config directories, skills.paths, and skills.urls.
  • The list goes to glob's ignore option, so excluded directories are pruned during the walk. Excluded matches never reach dirs or add(), so a copy cannot shadow the canonical skill or raise a duplicate-name warning.
  • The default list is empty, so nothing changes without the key. The key is optional and adds no dependency.
  • Documented in packages/web/src/content/docs/skills.mdx. The schema description is in packages/core/src/v1/config/skills.ts.

Closes #59

Testing

  • packages/opencode/test/skill/skill.test.ts passes. Its two new cases use a project root holding a canonical review skill, a hidden per-host copy, and a .trash copy.
  • The excluded case fails on the base implementation and passes with this change. The unset case passes on both, as a preservation check.
  • Removing the scan's ignore wiring, or ignoring the configured list, each fails the excluded case.
  • Related suites pass: skill, config, system, and agent tests, and the core filesystem tests.
  • Typecheck passes for core, opencode, and sdk. Prettier is clean on the changed files.
  • The first-request probe described in Skill discovery: add a skills exclude so hidden and trash copies under ~/.claude/skills are not loaded #59 was not run for this change; it needs the live configuration.

Notes

Stock compatibility. Stock opencode's V1 config schema is strict (additionalProperties: false), so a shared opencode.json that sets skills.exclude fails validation on a stock build until this lands upstream. Do not add the key to a shared config until a build with this change ships.

🤖 Generated with Claude Code

Skill discovery scans ~/.claude/skills and ~/.agents/skills with dot:
true, so gstack's per-host copies (gstack/.openclaw, .factory, ...) and
skills/.trash load next to the canonical skills: about 60 of 86 listed
skills, roughly 53k chars of every first request (#59).
Only whole-root env flags exist today, and skills.paths only adds roots.

Plan:
- core: Glob.Options gains an optional ignore list passed to glob's
  ignore option. It prunes excluded directories during the walk, so a
  .trash tree is not read at all.
- core v1 config: ConfigSkillsV1 gains skills.exclude (string array),
  described in the schema. The v2 array form of skills lowers to
  {paths, urls} and cannot carry it, so the object form is the key.
- opencode skill: ScanState carries the configured excludes, so every
  scan honors them: global ~/.claude and ~/.agents, project ancestors,
  config directories, skills.paths and skills.urls. Patterns match each
  SKILL.md path relative to the root it was found under, so one list
  reads the same in every root (for example **/.trash/**).
- An excluded match is dropped before it reaches dirs or add(), so it
  cannot shadow the canonical skill or raise a duplicate-name warning.
- Without the setting the default list is empty and nothing changes.
- Test (skill.test.ts): a home root with a canonical review skill, a
  gstack/.openclaw copy and a .trash copy, all named review. With the
  setting, dirs() holds only the canonical directory and all() only its
  location. Without it, dirs() holds all three directories. Red proof:
  the new cases run against the unchanged tree before implementing.
- Docs: a short skills.exclude section in packages/web skills.mdx.
- Not changed: the v2 skill plugin in packages/core never scans ~/.claude
  or ~/.agents, so it has no copies to exclude.
- Deferred to publication: CHANGESET.md needs this PR's number under
  "Fork changes", and CI reads it from the PR head, so that entry is
  added once the PR exists.

Claude-Session: https://claude.ai/code/session_015hTsruVJZ4rc4nVuPj1KgU
A project root holds a canonical review skill, a gstack/.openclaw copy
and a .trash copy, all named review. With skills.exclude set to
**/.trash/** and **/gstack/.*/**, dirs() must hold only the canonical
directory and all() only its location. Without the setting, dirs() must
hold all three directories.

Red on the base before the implementation commit: the excluded case
fails because dirs() still holds the .trash and gstack/.openclaw
directories, and the unset case passes as a preservation check.

Claude-Session: https://claude.ai/code/session_015hTsruVJZ4rc4nVuPj1KgU
skills.exclude takes glob patterns for SKILL.md files. Each pattern is
matched against the path relative to the root a scan started from, and
every scan honors the list: global ~/.claude and ~/.agents, project
ancestors, config directories, skills.paths and skills.urls.

The list goes to glob's ignore option, so excluded directories are
pruned during the walk, and an excluded match never reaches dirs or
add(). A copy under gstack/.openclaw or .trash therefore cannot shadow
the canonical skill or raise a duplicate-name warning.

The default is an empty list, so nothing changes without the setting.

The regression fixture lives in a project .claude/skills root rather
than a home root that the plan named: the global Global.home is
captured when the test layer builds, so withHome cannot redirect it
(filed as #157).

Refs #59

Claude-Session: https://claude.ai/code/session_015hTsruVJZ4rc4nVuPj1KgU
Only the skills.exclude hunks are taken from the generator output, in
packages/sdk/openapi.json and packages/sdk/js/src/v2/gen/types.gen.ts.

packages/sdk/openapi.json was already behind the source for
activeAssistantMessageID (packages/opencode/src/server/routes/instance/
httpapi/public.ts) before this change. That drift is not regenerated
here so this commit stays scoped to skills.exclude.

Claude-Session: https://claude.ai/code/session_015hTsruVJZ4rc4nVuPj1KgU
@brentyates-swx

Copy link
Copy Markdown
Author

Session stamp for the PR that this session opened:

Adds the Features entry that the changeset check requires for this PR.
The PR number is only known once the PR exists, so the entry follows the
PR creation.

Claude-Session: https://claude.ai/code/session_015hTsruVJZ4rc4nVuPj1KgU
Brings in the #155 revert (#163) before this PR merges. The reviewed
head 394ec55 stays on the first parent.

Claude-Session: https://claude.ai/code/session_015hTsruVJZ4rc4nVuPj1KgU
@brentyates-swx
brentyates-swx merged commit 8d536e3 into swxtch Oct 10, 2026
3 checks passed
@brentyates-swx
brentyates-swx deleted the fm/opencode-skills-exclude-59 branch October 10, 2026 03:32
brentyates-swx added a commit that referenced this pull request Oct 11, 2026
Brings in 8d536e3 (#159) so the branch is current with the default branch.
It does not touch the SQLite driver, the queue, or the lock tests.

Refs #162

Claude-Session: https://claude.ai/code/session_01LrS7BTRTnc6eYrSxBFoWAo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Skill discovery: add a skills exclude so hidden and trash copies under ~/.claude/skills are not loaded

1 participant