#18234 · @LanternCX · opened Mar 19, 2026 at 12:24 PM UTC · last updated Mar 21, 2026 at 4:07 PM UTC

feat(skill): add built-in using-opencode skill

appfeat
57
+111124 files

Score breakdown

Impact

8.0

Clarity

9.0

Urgency

5.0

Ease Of Review

8.0

Guidelines

9.0

Readiness

8.0

Size

4.0

Trust

5.0

Traction

0.0

Summary

This PR adds a built-in using-opencode skill to guide the model when creating OpenCode-native objects, particularly in fresh repositories. It addresses an inconsistency where the model struggles without existing scaffolding. The change is focused and includes tests.

Open in GitHub

Description

Issue for this PR

Closes #18232

Type of change

  • [ ] Bug fix
  • [x] New feature
  • [ ] Refactor / code improvement
  • [ ] Documentation

What does this PR do?

This adds a built-in skill named using-opencode.

The problem this is trying to solve is that OpenCode can already discover skills well, but when it is asked to create OpenCode-native objects in natural language it can still end up guessing where things should go if the repo has no existing scaffolding to imitate.

The change keeps the implementation narrow:

  • register using-opencode as a built-in skill in the existing skill loader
  • keep it out of Skill.dirs() since it is not user-managed content
  • expose it through the normal skill listing / skill tool path
  • add tests to verify discovery and execution

This works because the model now has a built-in OpenCode-specific guidance skill available even in fresh repos, without requiring the user to install anything first.

How did you verify your code works?

Ran in packages/opencode:

  • bun test test/skill/skill.test.ts test/tool/skill.test.ts
  • bun typecheck

Screenshots / recordings

Not a UI change.

Checklist

  • [x] I have tested my changes locally
  • [x] I have not included unrelated changes in this PR

Linked Issues

#18232 [FEATURE]: Add a built-in using-opencode skill for OpenCode-native scaffolding

View issue

Comments

No comments.

Changed Files

packages/opencode/src/skill/builtin/using-opencode/SKILL.md

+430
@@ -0,0 +1,43 @@
---
name: using-opencode
description: Use this before creating or editing OpenCode skills, commands, agents, plugins, or config so you can confirm the official OpenCode docs and file locations first.
---
# Using OpenCode
Use this when the task is about OpenCode itself rather than the user's app code.
Typical triggers:
- creating or editing an OpenCode `skill`
- creating or editing an OpenCode `command`
- creating or editing an OpenCode `agent`
- creating or editing an OpenCode `plugin`
- deciding where OpenCode-owned files should live
- checking whether project-level or global configuration is the better fit
Before you make changes, look up the current OpenCode docs and verify the expected location and format.
Docs to check first:
- Skills: `https://opencode.ai/docs/skills`
- Commands: `https://opencode.ai/docs/commands`
- Agents: `https://opencode.ai/docs/agents`
- Plugins: `https://opencode.ai/docs/plugins`
- Config and directory conventions: `https://opencode.ai/docs/config`
While reading the docs, confirm these points before editing files:
1. Whether the object should live in the project or global config.
2. Which direct

packages/opencode/src/skill/index.ts

+83
@@ -1,6 +1,6 @@
import os from "os"
import path from "path"
import { pathToFileURL } from "url"
import { fileURLToPath, pathToFileURL } from "url"
import z from "zod"
import { Effect, Layer, ServiceMap } from "effect"
import { NamedError } from "@opencode-ai/util/error"
@@ -24,6 +24,7 @@ export namespace Skill {
const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md"
const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md"
const SKILL_PATTERN = "**/SKILL.md"
const BUILTIN = [fileURLToPath(new URL("./builtin/using-opencode/SKILL.md", import.meta.url))]
export const Info = z.object({
name: z.string(),
@@ -68,7 +69,7 @@ export namespace Skill {
readonly available: (agent?: Agent.Info) => Effect.Effect<Info[]>
}
const add = async (state: State, match: string) => {
const add = async (state: State, match: string, opts?: { dir?: boolean }) => {
const md = await ConfigMarkdown.parse(match).catch(async (err) => {
const message = ConfigMarkdown.FrontmatterError.isInstance(err)
? err.data.message
@@ -92,7 +93,7 @@ export namespace Skill {
})
}
state.dirs.add(path.dirname(match))
if (opts?.dir !== false) s

packages/opencode/test/skill/skill.test.ts

+239
@@ -49,7 +49,7 @@ Instructions here.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((skill) => skill.name !== "using-opencode")
expect(skills.length).toBe(1)
const testSkill = skills.find((s) => s.name === "test-skill")
expect(testSkill).toBeDefined()
@@ -127,7 +127,7 @@ description: Second test skill.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((skill) => skill.name !== "using-opencode")
expect(skills.length).toBe(2)
expect(skills.find((s) => s.name === "skill-one")).toBeDefined()
expect(skills.find((s) => s.name === "skill-two")).toBeDefined()
@@ -153,12 +153,26 @@ Just some content without YAML frontmatter.
await Instance.provide({
directory: tmp.path,
fn: async () => {
const skills = await Skill.all()
const skills = (await Skill.all()).filter((skill) => skill.name !== "using-opencode")
expect(skills).toEqual([])
},
})
})
test("always includes built-in usi

packages/opencode/test/tool/skill.test.ts

+370
@@ -23,6 +23,18 @@ afterEach(async () => {
})
describe("tool.skill", () => {
test("description lists built-in using-opencode skill", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const tool = await SkillTool.init()
expect(tool.description).toContain("**using-opencode**")
},
})
})
test("description lists skill location URL", async () => {
await using tmp = await tmpdir({
git: true,
@@ -164,4 +176,29 @@ Use this skill.
process.env.OPENCODE_TEST_HOME = home
}
})
test("execute returns built-in using-opencode skill content", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const tool = await SkillTool.init()
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const ctx: Tool.Context = {
...baseCtx,
ask: async (req) => {
requests.push(req)
},
}
const result = await tool.execute({ name: "usi