Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ export namespace Config {
Global.Path.config,
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
targets: [".opencode", ".claude"],
start: Instance.directory,
stop: Instance.worktree,
}),
)),
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
targets: [".opencode", ".claude"],
start: Global.Path.home,
stop: Global.Path.home,
}),
Expand All @@ -92,7 +92,9 @@ export namespace Config {

const promises: Promise<void>[] = []
for (const dir of unique(directories)) {
await assertValid(dir)
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
await assertValid(dir)
}

if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
for (const file of ["opencode.jsonc", "opencode.json"]) {
Expand Down Expand Up @@ -147,7 +149,7 @@ export namespace Config {
}
})

const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools"].join(",")}}/`)
const INVALID_DIRS = new Bun.Glob(`{${["agents", "commands", "plugins", "tools", "skill"].join(",")}}/`)
async function assertValid(dir: string) {
const invalid = await Array.fromAsync(
INVALID_DIRS.scan({
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/skill/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export namespace Skill {
}),
)

const SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
const SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")

export const state = Instance.state(async () => {
const directories = await Config.directories()
Expand Down
64 changes: 32 additions & 32 deletions packages/opencode/test/skill/skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import path from "path"

test("discovers skills from .opencode/skill/ directory", async () => {
test("discovers skills from .opencode/skills/ directory", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const skillDir = path.join(dir, ".opencode", "skill", "test-skill")
const skillDir = path.join(dir, ".opencode", "skills", "test-skill")
await Bun.write(
path.join(skillDir, "SKILL.md"),
`---
Expand All @@ -32,16 +32,16 @@ Instructions here.
expect(skills.length).toBe(1)
expect(skills[0].name).toBe("test-skill")
expect(skills[0].description).toBe("A test skill for verification.")
expect(skills[0].location).toContain("skill/test-skill/SKILL.md")
expect(skills[0].location).toContain("skills/test-skill/SKILL.md")
},
})
})

test("discovers multiple skills from .opencode/skill/ directory", async () => {
test("discovers multiple skills from .opencode/skills/ directory", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const skillDir = path.join(dir, ".opencode", "skill", "my-skill")
const skillDir = path.join(dir, ".opencode", "skills", "my-skill")
await Bun.write(
path.join(skillDir, "SKILL.md"),
`---
Expand Down Expand Up @@ -69,7 +69,7 @@ test("skips skills with missing frontmatter", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const skillDir = path.join(dir, ".opencode", "skill", "no-frontmatter")
const skillDir = path.join(dir, ".opencode", "skills", "no-frontmatter")
await Bun.write(
path.join(skillDir, "SKILL.md"),
`# No Frontmatter
Expand Down Expand Up @@ -101,31 +101,31 @@ test("returns empty array when no skills exist", async () => {
})
})

// test("discovers skills from .claude/skills/ directory", async () => {
// await using tmp = await tmpdir({
// git: true,
// init: async (dir) => {
// const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
// await Bun.write(
// path.join(skillDir, "SKILL.md"),
// `---
// name: claude-skill
// description: A skill in the .claude/skills directory.
// ---
test("discovers skills from .claude/skills/ directory", async () => {
await using tmp = await tmpdir({
git: true,
init: async (dir) => {
const skillDir = path.join(dir, ".claude", "skills", "claude-skill")
await Bun.write(
path.join(skillDir, "SKILL.md"),
`---
name: claude-skill
description: A skill in the .claude/skills directory.
---

// # Claude Skill
// `,
// )
// },
// })
# Claude Skill
`,
)
},
})

// await Instance.provide({
// directory: tmp.path,
// fn: async () => {
// const skills = await Skill.all()
// expect(skills.length).toBe(1)
// expect(skills[0].name).toBe("claude-skill")
// expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
// },
// })
// })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
expect(skills.length).toBe(1)
expect(skills[0].name).toBe("claude-skill")
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
},
})
})
10 changes: 5 additions & 5 deletions packages/web/src/content/docs/skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ Skills are loaded on-demand via the native `skill` tool—agents see available s
Create one folder per skill name and put a `SKILL.md` inside it.
OpenCode searches these locations:

- Project config: `.opencode/skill/<name>/SKILL.md`
- Global config: `~/.opencode/skill/<name>/SKILL.md`
- Project config: `.opencode/skills/<name>/SKILL.md`
- Global config: `~/.opencode/skills/<name>/SKILL.md`
- Claude-compatible: `.claude/skills/<name>/SKILL.md`

---

## Understand discovery

For project-local paths, OpenCode walks up from your current working directory until it reaches the git worktree.
It loads any matching `skill/*/SKILL.md` in `.opencode/` and any matching `.claude/skills/*/SKILL.md` along the way.
It loads any matching `skills/*/SKILL.md` in `.opencode/` and any matching `.claude/skills/*/SKILL.md` along the way.

Global definitions are also loaded from `~/.opencode/skill/*/SKILL.md`.
Global definitions are also loaded from `~/.opencode/skills/*/SKILL.md`.

---

Expand Down Expand Up @@ -70,7 +70,7 @@ Keep it specific enough for the agent to choose correctly.

## Use an example

Create `.opencode/skill/git-release/SKILL.md` like this:
Create `.opencode/skills/git-release/SKILL.md` like this:

```markdown
---
Expand Down