From 8b40ecae08fdc481c339001651a369dffccbabb5 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Wed, 23 Sep 2026 13:33:26 -0400 Subject: [PATCH 1/4] skills: OpenCode's and Goose's folders are read too Issue #1277's day-one list names .opencode/skills and .goose/skills beside the six folders discovery already read. They are appended after .gemini, so a name held in any of the first six still owns it and the later copy is kept in the result marked shadowed, in both the project and the home scope. Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 5d6f36f63993b419db41bdcb971e6c37c94c9468) --- internal/skills/skills.go | 2 ++ internal/skills/skills_test.go | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/internal/skills/skills.go b/internal/skills/skills.go index 5588b594f1..2da86a4658 100644 --- a/internal/skills/skills.go +++ b/internal/skills/skills.go @@ -70,6 +70,8 @@ var skillRoots = []string{ ".codex/skills", ".cursor/skills", ".gemini/skills", + ".opencode/skills", + ".goose/skills", } // RootCodexSystem is the folder Codex installs its own bundled skills into. diff --git a/internal/skills/skills_test.go b/internal/skills/skills_test.go index e95ebf4cc3..50deb009d3 100644 --- a/internal/skills/skills_test.go +++ b/internal/skills/skills_test.go @@ -1,6 +1,7 @@ package skills import ( + "os" "path/filepath" "strings" "testing" @@ -222,3 +223,40 @@ func TestDiscoverNeedsAtLeastOneBase(t *testing.T) { t.Fatal("Discover with no directories at all did not error") } } + +// TestDiscoverReadsOpenCodeAndGooseFolders pins the two folders #1277 adds to +// the day-one list after Gemini's, in both scopes, and their rank: a name kept +// in an earlier folder of the same scope owns it over theirs. +func TestDiscoverReadsOpenCodeAndGooseFolders(t *testing.T) { + project := t.TempDir() + home := t.TempDir() + writeSkill(t, filepath.Join(project, ".opencode", "skills", "open-helper"), "open-helper") + writeSkill(t, filepath.Join(home, ".goose", "skills", "goose-helper"), "goose-helper") + writeSkill(t, filepath.Join(home, ".gemini", "skills", "shared"), "shared") + writeSkill(t, filepath.Join(home, ".goose", "skills", "shared"), "shared") + + found := discover(t, project, home) + open, ok := byDir(found, filepath.Join(".opencode", "skills", "open-helper")) + if !ok || open.Scope != ScopeProject || open.Shadowed || open.Root != ".opencode/skills" { + t.Fatalf("the .opencode project skill is missing or wrong: %+v", found) + } + goose, ok := byDir(found, filepath.Join(".goose", "skills", "goose-helper")) + if !ok || goose.Scope != ScopeUser || goose.Shadowed || goose.Root != ".goose/skills" { + t.Fatalf("the .goose user skill is missing or wrong: %+v", found) + } + loser, ok := byDir(found, filepath.Join(".goose", "skills", "shared")) + if !ok || !loser.Shadowed { + t.Fatalf("the .goose copy of a name .gemini holds is not shadowed: %+v", found) + } +} + +func writeSkill(t *testing.T, dir, name string) { + t.Helper() + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + body := "---\nname: " + name + "\ndescription: A skill for the discovery test.\n---\nBody.\n" + if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } +} From fe2f229063de39056f3ebbf064c50e0dd5e451c1 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Wed, 30 Sep 2026 13:58:32 -0400 Subject: [PATCH 2/4] skills: OpenCode's and Goose's own home folders are read too OpenCode keeps a person's global skills in ~/.config/opencode/skills and Goose in ~/.config/goose/skills, so the dot folders named after each tool are empty on a machine that uses them. Both are now read in the home scope only, after the shared folders and before plugin and bundled skills, so a name kept by hand anywhere earlier still owns it. A project's own .config folder is not read. The comments no longer count the folders, because the count had already drifted once. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/skills/plugins.go | 2 +- internal/skills/skills.go | 34 ++++++++++++---- internal/skills/skills_test.go | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/internal/skills/plugins.go b/internal/skills/plugins.go index 317bd2d608..b69bbb6f7c 100644 --- a/internal/skills/plugins.go +++ b/internal/skills/plugins.go @@ -47,7 +47,7 @@ const RootClaudePlugins = ".claude/plugins" // one identity every reader keys on (internal/store's Fact.SkillName), and // the agentskills.io name has no colon in its alphabet. So a plugin skill // keeps its bare name and settles a collision by rank instead: every skill in -// the six hand-kept folders of a scope owns the name over a plugin's, because +// the hand-kept folders of a scope owns the name over a plugin's, because // a skill somebody placed by hand is the one they meant, and between two // plugins the one whose key sorts first owns it. The loser stays in the result // marked Shadowed, like every other loser. diff --git a/internal/skills/skills.go b/internal/skills/skills.go index 2da86a4658..d6d9e1efff 100644 --- a/internal/skills/skills.go +++ b/internal/skills/skills.go @@ -57,12 +57,12 @@ const ( // folder are the common way a skill reaches several harnesses at once, and a // scan that skipped links saw none of those. // -// TWO MORE SOURCES FOLLOW THESE SIX WITHIN EACH SCOPE, and they come last on -// purpose (see [Discover]): the skills that arrived inside an installed and +// TWO MORE SOURCES FOLLOW THESE FOLDERS WITHIN EACH SCOPE, and they come last +// on purpose (see [Discover]): the skills that arrived inside an installed and // enabled Claude Code plugin ([RootClaudePlugins], plugins.go), and the skills // Codex ships with itself ([RootCodexSystem]). Nobody placed either of them -// by hand, so any skill a person did place by hand, in any of the six folders, -// owns the name over them. +// by hand, so any skill a person did place by hand, in any of the hand-kept +// folders, owns the name over them. var skillRoots = []string{ ".codeaf/skills", ".agents/skills", @@ -74,8 +74,20 @@ var skillRoots = []string{ ".goose/skills", } +// homeSkillRoots are hand-kept folders that exist ONLY in the home directory, +// read after [skillRoots] in the user scope. OpenCode and Goose keep a +// person's own skills under ~/.config rather than in a dot folder named after +// the tool, so ~/.opencode/skills and ~/.goose/skills are empty on a machine +// that uses them and these are where their global skills actually live. They +// are never read under a project: a project's .config folder is somebody's +// application settings, not a place either tool looks for skills. +var homeSkillRoots = []string{ + ".config/opencode/skills", + ".config/goose/skills", +} + // RootCodexSystem is the folder Codex installs its own bundled skills into. -// It sits INSIDE .codex/skills, where the six-root scan sees it as one child +// It sits INSIDE .codex/skills, where the folder scan sees it as one child // with no SKILL.md and passes over it, so it is read as a root of its own. // // IT RANKS LAST IN ITS SCOPE, below the plugin skills too. A system skill is @@ -125,7 +137,7 @@ type Skill struct { // Discover scans the conventional skill folders under one project directory // and one home directory, in issue #1277's order, and returns what it found: -// within each scope the six hand-kept folders, then the skills of every +// within each scope the hand-kept folders, then the skills of every // installed and enabled Claude Code plugin, then Codex's bundled skills — // every folder that holds a SKILL.md, winners first, losers marked Shadowed, // and unreadable ones carried with a Warning rather than dropped. It never @@ -204,11 +216,19 @@ func Discover(opts Options) ([]Skill, error) { for _, root := range skillRoots { collect(filepath.Join(base.dir, root), root, scope, "") } + // The home-only folders follow the shared ones in whichever base IS + // the home directory: the user scope, or the project scope when codeaf + // was opened in the home directory itself and the two bases folded. + if homeDir != "" && base.dir == absolute(homeDir) { + for _, root := range homeSkillRoots { + collect(filepath.Join(base.dir, root), root, scope, "") + } + } // THE PLUGIN SKILLS, after every hand-kept folder in the scope and // before the harness's own bundled skills. A project scope carries // the plugins installed for this project; when the home directory // folded into the project base above, it carries the person's own - // plugins after them too, the same way it already carries their six + // plugins after them too, the same way it already carries their hand-kept // home folders. plugins := userPlugins if scope == ScopeProject { diff --git a/internal/skills/skills_test.go b/internal/skills/skills_test.go index 50deb009d3..f15bddbc66 100644 --- a/internal/skills/skills_test.go +++ b/internal/skills/skills_test.go @@ -250,6 +250,78 @@ func TestDiscoverReadsOpenCodeAndGooseFolders(t *testing.T) { } } +// TestDiscoverReadsOpenCodeAndGooseHomeFolders pins where the two tools keep a +// person's own skills: under ~/.config, not in a dot folder in the home. Both +// are read in the user scope with their own root name, and neither is read +// under a project, whose .config folder is not a skills location. +func TestDiscoverReadsOpenCodeAndGooseHomeFolders(t *testing.T) { + project := t.TempDir() + home := t.TempDir() + writeSkill(t, filepath.Join(home, ".config", "opencode", "skills", "open-global"), "open-global") + writeSkill(t, filepath.Join(home, ".config", "goose", "skills", "goose-global"), "goose-global") + writeSkill(t, filepath.Join(project, ".config", "opencode", "skills", "not-a-skill-place"), "not-a-skill-place") + + found := discover(t, project, home) + open, ok := byDir(found, filepath.Join(".config", "opencode", "skills", "open-global")) + if !ok || open.Scope != ScopeUser || open.Shadowed || open.Root != ".config/opencode/skills" { + t.Fatalf("OpenCode's global skill is missing or wrong: %+v", found) + } + goose, ok := byDir(found, filepath.Join(".config", "goose", "skills", "goose-global")) + if !ok || goose.Scope != ScopeUser || goose.Shadowed || goose.Root != ".config/goose/skills" { + t.Fatalf("Goose's global skill is missing or wrong: %+v", found) + } + if stray, ok := byDir(found, "not-a-skill-place"); ok { + t.Fatalf("a project's .config folder was read as a skills folder: %+v", stray) + } +} + +// TestHomeOnlyFoldersRankAfterTheSharedOnes pins precedence: a name kept in a +// shared hand-kept folder of the same scope owns it over the ~/.config copy, +// and any project skill owns it over every home one. +func TestHomeOnlyFoldersRankAfterTheSharedOnes(t *testing.T) { + project := t.TempDir() + home := t.TempDir() + writeSkill(t, filepath.Join(home, ".claude", "skills", "shared"), "shared") + writeSkill(t, filepath.Join(home, ".config", "goose", "skills", "shared"), "shared") + writeSkill(t, filepath.Join(project, ".goose", "skills", "mine"), "mine") + writeSkill(t, filepath.Join(home, ".config", "opencode", "skills", "mine"), "mine") + + found := discover(t, project, home) + if winner, ok := byDir(found, filepath.Join(".claude", "skills", "shared")); !ok || winner.Shadowed { + t.Fatalf("the ~/.claude copy lost its name to a ~/.config copy: %+v", found) + } + if loser, ok := byDir(found, filepath.Join(".config", "goose", "skills", "shared")); !ok || !loser.Shadowed { + t.Fatalf("the ~/.config/goose copy of a name ~/.claude holds is not shadowed: %+v", found) + } + if winner, ok := byDir(found, filepath.Join(".goose", "skills", "mine")); !ok || winner.Shadowed || winner.Scope != ScopeProject { + t.Fatalf("the project skill lost its name to a home one: %+v", found) + } + if loser, ok := byDir(found, filepath.Join(".config", "opencode", "skills", "mine")); !ok || !loser.Shadowed { + t.Fatalf("the ~/.config/opencode copy of a project skill's name is not shadowed: %+v", found) + } +} + +// TestHomeOnlyFoldersWhenOpenedInTheHomeItself pins the folded case: codeaf +// opened in the home directory reads the ~/.config folders once, not twice. +func TestHomeOnlyFoldersWhenOpenedInTheHomeItself(t *testing.T) { + home := t.TempDir() + writeSkill(t, filepath.Join(home, ".config", "opencode", "skills", "open-global"), "open-global") + + found := discover(t, home, home) + count := 0 + for _, skill := range found { + if strings.HasSuffix(skill.Dir, filepath.Join(".config", "opencode", "skills", "open-global")) { + count++ + if skill.Shadowed { + t.Fatalf("the only copy of a skill is marked shadowed: %+v", skill) + } + } + } + if count != 1 { + t.Fatalf("a ~/.config skill was reported %d times with codeaf opened in the home: %+v", count, found) + } +} + func writeSkill(t *testing.T, dir, name string) { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { From 6ba9a3d4a0bdfd05b3a158631cdb4f4b25d8ad43 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Wed, 30 Sep 2026 13:58:32 -0400 Subject: [PATCH 3/4] manual: skills from OpenCode and Goose are read where those tools keep them The page listed six folders. It now names .opencode/skills and .goose/skills beside them, and the two ~/.config folders that are read in the home only, so "can codeaf use my Goose skills" reaches an answer that says yes and where. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/manual/chat/skills-from-other-tools.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/internal/manual/chat/skills-from-other-tools.md b/internal/manual/chat/skills-from-other-tools.md index 5718ea191d..e8d595bfc2 100644 --- a/internal/manual/chat/skills-from-other-tools.md +++ b/internal/manual/chat/skills-from-other-tools.md @@ -1,9 +1,9 @@ -# Skills from Claude Code, Codex and other tools +# Skills from Claude Code, Codex, OpenCode, Goose and other tools -## Can you use my Claude Code and Codex skills +## Can you use my Claude Code, Codex, OpenCode or Goose skills Yes, directly. Any skill you already installed for Claude Code, Codex, Cursor, -Gemini or another agentskills.io tool — a folder holding a `SKILL.md` whose +Gemini, OpenCode, Goose or another agentskills.io tool — a folder holding a `SKILL.md` whose frontmatter has a `name` and a `description` — is read where it lives. Nothing is copied or reinstalled. Each launch reads the folders again before the first message, so a skill you add or edit shows up the next time you open codeaf. @@ -20,10 +20,13 @@ off. Under the project folder and under your home folder, in this order: - `.codeaf/skills`, `.agents/skills`, `.claude/skills`, `.codex/skills`, - `.cursor/skills`, `.gemini/skills` — each direct child folder holding a - `SKILL.md` is one skill. A child that is a link to a folder counts too, which - is how installers that keep one copy and link it into every tool's folder - reach codeaf. + `.cursor/skills`, `.gemini/skills`, `.opencode/skills`, `.goose/skills` — + each direct child folder holding a `SKILL.md` is one skill. A child that is a + link to a folder counts too, which is how installers that keep one copy and + link it into every tool's folder reach codeaf. +- In your home folder only, after those: `~/.config/opencode/skills` and + `~/.config/goose/skills`, which is where OpenCode and Goose keep the skills + you installed for yourself rather than for one project. - The skills of every Claude Code plugin that is installed and enabled. - Codex's own bundled skills, in `.codex/skills/.system`. From 9f945502bd11db630276020c9ea8322f8afcafee Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Wed, 30 Sep 2026 13:58:58 -0400 Subject: [PATCH 4/4] changes: skills from OpenCode and Goose Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/changes/unreleased/1691-skills-opencode-goose.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/changes/unreleased/1691-skills-opencode-goose.md diff --git a/docs/changes/unreleased/1691-skills-opencode-goose.md b/docs/changes/unreleased/1691-skills-opencode-goose.md new file mode 100644 index 0000000000..b5c4f16d7f --- /dev/null +++ b/docs/changes/unreleased/1691-skills-opencode-goose.md @@ -0,0 +1,11 @@ +--- +kind: added +title: skills kept for OpenCode and Goose are read too, including their ~/.config folders +pr: 1691 +surface: [chat, resident] +invalidates: + - "codeaf read skills from six folders (.codeaf, .agents, .claude, .codex, .cursor, .gemini under skills/). It also reads .opencode/skills and .goose/skills in the project and the home, and ~/.config/opencode/skills and ~/.config/goose/skills in the home only." +--- +OpenCode and Goose keep a person's global skills under ~/.config rather than in a +dot folder named after the tool, so those are read in the home scope, after every +shared folder, where a name kept by hand earlier still owns it.