Writing skills for AI assistants: where to put them, five complete examples
What a skill is
A skill is a reusable packet of instructions that teaches an AI
assistant how to handle one class of tasks: which tools to call, which
files to read first, what output format to produce. Unlike a one-off
prompt, a skill is stored, versioned and loaded automatically whenever
its trigger matches.
Think of it as onboarding documentation for a colleague who forgets
everything between sessions: precise, example-driven, and short enough
to actually be read. In OpenCode the agent
sees only id, name and description at each step, then loads the full
body on demand with the skill tool — or you load
it directly with @skill-id. Behavior that must
hold across sessions belongs in skills, one-off wishes belong in the
prompt. Identities with their own model and permissions are
agents, not skills.
Where to put skills
Skills are model-independent: Muse Spark 1.3 Free, GPT 6 Luna and Claude Opus 5.5 all read from the same places. What differs is how strictly each model follows a long skill — see Skills per model. Put team skills in the project so they are committed, personal helpers in the global dir.
- Project, recommended: .opencode/skills/ — discovered from the current dir up to the project root, every level counts.
- Global, personal: ~/.config/opencode/skills/ — applies to every project of the user.
- Compat, auto-read: .claude/skills/ and .agents/skills/, global first then project — handy when the same repo also serves other tools.
- Extra dirs or team catalog: skills array in opencode.jsonc — local dirs plus HTTP catalogs with index.json.
Two file shapes, same registry. Either a flat file at the source root or a directory with an exact SKILL.md anywhere below it. The path gives the id, the frontmatter name is only a label:
skills/review.md -> id: review skills/git-release/SKILL.md -> id: git-release skills/teams/release/SKILL.md -> id: release
Extra entries in config, resolved from the active working dir, home expansion allowed, arrays merge across configs:
{ "$schema": "https://opencode.ai/config.json", "skills": ["./team-skills", "~/shared/opencode-skills"] }
Precedence is by id, later source wins: built-ins, then .claude, then .agents, then global opencode/skills, then project .opencode/skills from root toward the current dir, then explicit skills entries. Avoid duplicate ids unless you mean to override. Full layout with links is under Real project skeleton, gating per agent under Gating skills per agent.
Anatomy of a good skill
Every skill needs four parts. A name and one-line description so the
assistant can decide relevance. Trigger conditions: when to load it
and, equally important, when not to. The procedure itself as numbered
steps, with exact commands rather than vague advice. And two or three
examples showing input, actions and expected output.
OpenCode frontmatter is small and optional, but description decides
discovery — no description means never advertised. Set
opencode/autoinvoke: false only to hide a helper
that other skills include by hand:
--- name: Git Release description: Prepare release notes, version bumps, and GitHub releases --- ## Workflow 1. Read references/release-policy.md. 2. Summarize merged changes since the previous tag. 3. Propose the version bump before changing files.
Paths inside the skill are relative to the dir with SKILL.md, so keep scripts and references beside it. The advertised list shows only id, name and description plus up to ten supporting paths, never the full body. What kills skills: generic wisdom, missing negative cases, and length. If it does not fit on one screen, split it into two skills.
Five complete examples
Copy these as starting points. Each one is a full file body, not a fragment. All five live in the tree under Real project skeleton.
Example 1 — flat commit style. File .opencode/skills/commit-style.md, id commit-style. Teaches message format for the whole repo in 12 lines:
--- name: Commit Style description: Write repo commit messages in Area: change form under 70 chars --- ## Use when User asks to commit, stage, or push the changes. ## Do not use for Code review,Unstaged debug logs, release notes. ## Workflow 1. Run git status --short and git diff --stat. Never commit unrelated files. 2. Message form: Area: specific change, under 70 chars. Example: Auth: reject expired tokens with 401. 3. Stage only the agreed paths, show the staged summary, wait for approval. 4. Commit with the approved message, no push unless asked.
Invoke with @commit-style Write a commit for the auth fix. Happy path: two changed files produce one staged commit. Edge case: mixed unrelated files produce two separate proposals. Lookalike where it must stay silent: user asks to review a diff.
Example 2 — directory migration skill with helpers. File .opencode/skills/db-migrate/SKILL.md, id db-migrate:
--- name: DB Migrate description: Safe database migration with review and rollback plan --- ## Use when User mentions migration, migrates the DB, edits files under db/. ## Do not use for Plain SELECT questions, schema explainers, seed data design. ## Workflow 1. Read references/policy.md for naming and reversibility rules. 2. Run git diff --stat and show the planned SQL diff. Stop and wait for approval. 3. Run scripts/rollback.sh --dry-run and paste the output. 4. Apply the migration, then run npm test -- --tag migration. 5. Output: applied files, rollback command, test result. ## Example Input: Add unique index on users.email. Actions: propose db/014_unique_email.sql, show rollback DROP INDEX, dry run first. Output: three bullet lines with file, rollback, tests green.
Beside it: scripts/rollback.sh prints the reverse SQL without applying it, references/policy.md holds naming rules in 20 lines:
# policy.md: every migration has a down file, no raw DROP TABLE, # index names like idx_users_email, all SQL reviewed before apply.
The agent reads helpers only when the skill tells it to. Flat skills get no neighbor file list, so this directory shape is the right choice as soon as a skill needs more than one file.
Example 3 — flat release skill. File .opencode/skills/git-release.md, id git-release:
--- name: Git Release description: Prepare changelog, version bump, tag, and release notes --- ## Use when User asks for release, changelog, version bump, tag the release. ## Workflow 1. Run git log --oneline v1.4.0..HEAD and group into Features, Fixes, Docs. 2. Propose the semver bump with reasons. Wait for approval. 3. Update CHANGELOG.md and package.json, run npm test. 4. Tag v1.5.0 and draft notes with highlights first. ## Example Input: Prepare 1.5.0. Output: bump minor because of new export API, changelog draft, tag command ready.
Example 4 — test writer for cheap models. File .opencode/skills/test-writer/SKILL.md, id test-writer. Written for Muse Spark Free and GPT 6 Luna: short, imperative, one exact test command:
--- name: Test Writer description: Add focused unit tests for one module with repo test runner --- ## Use when User asks to add tests, cover a module, fix missing coverage. ## Do not use for E2E suites, load tests, manual QA plans. ## Workflow 1. Read the target module and glob sibling tests under tests/ for style. 2. Write one test file per module: happy path, one edge, one error. 3. Run npm test -- --run tests/auth.test.ts and paste failures. 4. Fix until green. Output: new file path, cases added, command result. ## Example Input: Cover src/auth.ts. Actions: read src/auth.ts, mirror tests/auth.test.ts style, three cases. Output: file created, 3 passed in output block.
Example 5 — strict review skill for the top model. File .opencode/skills/api-review/SKILL.md, id api-review. Longer rubric, only the Opus reviewer agent is allowed to load it (see Gating):
--- name: API Review description: Review API diffs for bugs, security, and missing tests in severity order --- ## Use when User asks to review a diff, audit an endpoint, check auth handling. ## Workflow 1. Run git diff --cached and list touched endpoints. 2. Check in order: auth bypass, injection, broken status codes, missing tests. 3. For each finding give file, line, severity, and a minimal fix. 4. Verdict: ship, fix first, or block. Never edit files, report only. ## Example Input: Review staged auth diff. Output: 1. High: missing 401 on expired token in src/auth.ts:42. 2. Medium: no test for refresh replay. Verdict: fix first.
Pair it with the reviewer agent in Agents examples that denies edit so review never mutates code.
Real project skeleton
Full layout of a project with all five skills plus shared team skills. Output of tree, every element links to the chapter where it is explained:
my-app/ ├── opencode.jsonc ├── .opencode/skills/ │ ├── commit-style.md │ ├── db-migrate/ │ │ ├── SKILL.md │ │ ├── scripts/ │ │ │ └── rollback.sh │ │ └── references/ │ │ └── policy.md │ ├── git-release.md │ ├── test-writer/ │ │ └── SKILL.md │ └── api-review/ │ └── SKILL.md ├── team-skills/ │ └── deploy/ │ └── SKILL.md └── .opencode/agents/ ├── reviewer.md └── builder.md
Read it top down: opencode.jsonc registers extra dirs and gates ids per agent, commit-style is the flat starter, db-migrate is the directory pattern with helpers, test-writer is tuned for cheap models, api-review is the strict Opus-only skill. The sibling agents dir holds the profiles that load these ids.
Aredel project map
The aredel repo is a large PHP site with over 7000 PHP files where URL maps 1:1 to file path. Repo root and docroot:
/home/andrei/github/aredel
container/src/
For performance put all team skills once at the repo root so every working dir under container/src discovers the same set without re-scan, and add one nested override only for AI articles. No global skills for aredel work — global entries widen the advertised list on every step and cost tokens on all three models. Full trio wiring is in Aredel trio: three models on one checkout.
aredel/ ├── .opencode/skills/ │ ├── aredel-map.md │ ├── aredel-page/ │ │ ├── SKILL.md │ │ └── references/ │ │ └── page-checklist.md │ ├── aredel-blocks/ │ │ ├── SKILL.md │ │ └── references/ │ │ └── block-classes.md │ ├── aredel-css.md │ └── aredel-verify.md ├── container/src/ai/.opencode/skills/ │ └── ai-style.md ├── opencode.jsonc └── AGENTS.md
Why this placement is fastest: one discovery root means one merge, not one merge per subdir. The model sees five short ids (aredel-map, aredel-page, aredel-blocks, aredel-css, aredel-verify) plus one area id (ai-style) only under container/src/ai/. Heavy notes stay in references/ and load only when the skill body tells the agent to read them. Prefix every description with Aredel: so filtering happens from the advertised list without loading bodies.
Aredel skill 1 — map, always on, tiny. File .opencode/skills/aredel-map.md, id aredel-map. Distilled from the internal page versus include rules, under 20 lines so even Spark Free follows it:
--- name: Aredel Map description: Aredel: decide page versus include before editing any PHP file --- ## Use when User asks to edit, fix, or create any file under container/src/. ## Do not use for CSS-only edits, Python tooling under container/tools/, test-only changes. ## Workflow 1. Basename matches _top_path, _menu, _inc, _footer fragment, arj, copy dup, validate_, demo, or PascalCase helper like RelatedArticles.php: hands off, it is a non-page. 2. Else check footer path: page must render bottomid via createFooter or terminal _footer include. If already covered, hands off. 3. Else it is a page candidate: URL equals src plus path, confirm in sitemap.xml before patching. ## Example Input: Fix src/ao/chessboard/game.php. Output: Non-page, game tooling, refuse footer patch, propose page-level fix instead.
Aredel skill 2 — page scaffold. File .opencode/skills/aredel-page/SKILL.md, id aredel-page. References hold the long class table so the body stays short:
--- name: Aredel Page description: Aredel: scaffold ArticlePage, BasePage, RelatedArticles, tcont and lesson nav --- ## Use when User asks for a new article, lesson, hub, or course index. ## Workflow 1. Read references/page-checklist.md for the class choice: ArticlePage for lessons, BasePage for hubs, DictPage for dictionary entries. 2. Create sibling RelatedArticles.php with articles plus breadcrumbs, wire tcont_urls, wire prev and next lesson. 3. Never use legacy 010_header, 020_header, hand TOC tables, or new Page. 4. Run php -l and CLI render with DOCUMENT_ROOT shim, then regenerate sitemap. ## Example Input: Add dev/ruby/course lesson 51. Output: ArticlePage file plus RelatedArticles entry plus tcont plus sitemap run.
Aredel skill 3 — blocks and trees. File .opencode/skills/aredel-blocks/SKILL.md, id aredel-blocks:
--- name: Aredel Blocks description: Aredel: code and command blocks, tree output with hyperlinked elements --- ## Use when User edits article body HTML between createHead and createFooter. ## Workflow 1. One shell command per cmd1, narrative in p2, output in cout1, Python in pycode, generic code in somecode. Never use pre code. 2. Tree output uses cout1 wspw with each element as link to its chapter anchor, same as ai-style pages. 3. First-mention Linux, Windows, Unix links only in prose p2, never in code or paths. 4. Keep tag balance: php -l plus render must stay exit 0. ## Example Input: Add project tree to a lesson. Output: cout1 wspw block with five linked elements plus render check.
Aredel skill 4 — CSS sync, hidden by default. File .opencode/skills/aredel-css.md, id aredel-css. Rare, so hide it from the advertised list and load by hand with @aredel-css:
--- name: Aredel CSS description: Aredel: sync a new code class from to_build to ao.css and aredel.css metadata: opencode/autoinvoke: false --- ## Workflow 1. Clone the sibling block in to_build/_blockquotes.css, rename class, label, header colors, append at EOF. 2. Mirror the identical minified line into ao.css and aredel.css, keep trailing newline. 3. Verify grep -o counts match sibling classes and render one page with the class.
Aredel skill 5 — verify before commit. File .opencode/skills/aredel-verify.md, id aredel-verify:
--- name: Aredel Verify description: Aredel: php -l, CLI render, venv tests, CRLF-safe commit --- ## Use when User asks to commit, verify, or finish aredel work. ## Workflow 1. Run php -l on touched files. Render touched pages via CLI shim, expect exit 0 and only REMOTE_ADDR noise. 2. Run container tests via tools/run-tests-venv.sh, never pip install into managed venv. 3. For tree-wide edits preserve CRLF where HEAD has CR, run check-reflow gate without SKIP on final state. 4. Regenerate sitemap after page add or remove and show grep -c loc count.
Nested override — AI articles only. File container/src/ai/.opencode/skills/ai-style.md, id ai-style. Loaded only when the working dir is under container/src/ai/: model pricing tables with a note to verify prices in docs, cross-links between opencode, skills and agents chapters, no secrets to Spark Free contributor tier. Keeps AI style out of the other 7000 pages.
Root wiring for speed. Root AGENTS.md in three lines points at the map first:
# Project instructions - Read @aredel-map before touching container/src PHP files. - Page work: @aredel-page plus @aredel-blocks. CSS: @aredel-css by hand. - Finish with @aredel-verify, then regenerate sitemap.
Gate by role so cheap models stay lean — triage sees only the map, builder sees page plus blocks plus verify, Opus reviewer sees verify plus the corruption patterns. See the full trio config in Aredel trio.
Skills per model
One location, three reading styles. Tune length and strictness to the model in Models that matter:
- Muse Spark 1.3 Free: short and imperative. One screen, numbered steps, exact commands, one happy-path example. Long policy prose gets skimmed — move details to references and point at one file per step. Best skills: commit-style, test-writer.
- GPT 6 Luna: checklist style works best. Preconditions, diff-before-write, test command, stop conditions. Good default for CI skills where every cent counts. Best skills: db-migrate, git-release.
- Claude Opus 5.5: the only model to trust with long multi-file skills: policy plus rubric plus negative cases. Still keep triggers tight — a bigger model makes bad triggers fail more eloquently, not less. Best skill: api-review.
Practical split from OpenCode workflows: Spark Free does drafts with @test-writer, Luna runs routine migrations with @db-migrate, Opus does the final pass with @api-review. Never send secrets to the free contributor tier — switch to paid or local for sensitive code. How the three share one checkout with per-agent visibility is shown in Three models, one project.
Gating skills per agent
Advertise every skill globally, then restrict per agent. Last matching rule wins. Example: everyone sees commit and tests, only the reviewer sees the strict review skill, the explore agent sees nothing to load:
{ "$schema": "https://opencode.ai/config.json", "permissions": [ { "action": "skill", "resource": "*", "effect": "allow" } ], "agents": { "reviewer": { "permissions": [ { "action": "skill", "resource": "*", "effect": "deny" }, { "action": "skill", "resource": "api-review", "effect": "allow" } ] }, "explore": { "permissions": [ { "action": "skill", "resource": "*", "effect": "deny" } ] } } }
Effects: allow loads silently, ask shows the skill but asks before loading, deny hides it from the model entirely. Agent blocks append after global rules. Full agent side is in How to build an agent.
Two gates: discovery vs permission
Loading a skill needs two independent gates to pass, and they answer different questions. Discovery asks where: is this skill advertised for the current working dir. Permission asks who: may this agent load it. Both must pass; either one fails and the model never sees the skill:
| Discovered here | Agent permission | What the model sees |
|---|---|---|
| yes | allow | Advertised, loads silently |
| yes | ask | Advertised, asks before loading |
| yes | deny | Invisible, same as absent |
| no | allow | Invisible, permission never fires |
There is also a third state: discovered and registered but hidden from the advertised list via opencode/autoinvoke: false. The skill stays loadable by hand with @skill-id — the aredel aredel-css skill works this way because CSS edits are rare and the skill should never spend context uninvited.
Example 1 — same permission, different dir. Builder is allowed ai-style, which lives in container/src/ai/.opencode/skills/ (see Aredel project map). Editing AI articles, builder sees the pricing-table checklist and loads it. Editing Ruby lessons, the skill is never advertised there, so the same allow-rule never fires and builder works with the other four skills. One permission config, location-dependent visibility, no extra work.
Example 2 — same dir, different role. In the repo root all five aredel skills are discovered, but triage is allowed only aredel-map, builder sees aredel-page plus aredel-blocks plus aredel-verify, and the Opus reviewer sees only aredel-verify. Same shelf, different visibility per role — the split from Three models, one project.
Example 3 — debugging a missing skill. The agent should know a skill but does not mention it: check discovery first (wrong dir, wrong filename — must be root-level *.md or exactly SKILL.md), then permission (a deny later in the merged list wins), then autoinvoke: false. Nine times out of ten it is the first two.
Team catalog over HTTP
For skills shared across repos, ship a catalog dir over HTTP with index.json at the base URL. Each entry lists safe relative files, must include SKILL.md or name.md, bump version to refresh caches:
{ "skills": [ { "name": "deploy", "version": "3", "files": ["SKILL.md", "references/checklist.md"] } ] }
Register it alongside local dirs:
{ "$schema": "https://opencode.ai/config.json", "skills": ["./team-skills", "https://example.com/skills/"] }
Downloaded dirs become source roots, so deploy/SKILL.md keeps id deploy. Keep ids lowercase kebab-case, unique across all sources, or later sources silently override earlier ones.
Testing and sharing
Test every new skill with three prompts before committing. Example for db-migrate:
- Happy path: @db-migrate Add unique index on users.email — must propose SQL, dry run, tests.
- Edge case: @db-migrate Rename users to accounts with zero downtime — must ask for a backfill plan, not blindly rename.
- Lookalike, must stay silent: Explain how SELECT uses the email index — skill must not fire. If it fires, tighten triggers.
Load manually with @skill-id to verify the body, then check the agent permission that gates it. Share as plain files next to the code they govern and link from team docs — the same single-entry principle as agents. Mention @db-migrate once instead of pasting the checklist into every prompt.
Trigger design
Triggers are half the skill. Bad trigger block, fires everywhere:
Use when: any database question.
Good trigger block, fires only on intent plus path:
## Use when User mentions migration, migrates the DB, edits files under db/. ## Do not use for Plain SELECT questions, schema explainers, seed data design.
Name the exact phrases, file paths and contexts that should load it, and just as explicitly list the near-misses where it must stay silent. Over-broad triggers teach the assistant to ignore the skill entirely. Good blocks name a dir, a verb, and two negatives. Test the negatives — they are the common failure.
Failure smells
Three smells say the skill, not the model, is broken, each with a fix:
- Fires on wrong tasks: triggers too broad. Fix: add two Do not use for lines with the exact lookalikes from logs.
- Fires right but output ignores procedure: steps too vague. Fix: replace advice with exact commands — git diff --cached, npm test -- --run tests/auth.test.ts — plus one input-output example.
- Two skills contradict: overlapping ids or policies. Fix: merge them or gate by agent so only one is visible per role.
Fix the skill first; a bigger model only makes bad instructions fail more eloquently. When the fix needs a different identity or tool set rather than better text, promote it to an agent instead.
Article author: Arthur Isaev
| AI | |
| Writing skills for AI assistants | |
| Creating and developing agents | |
| Models for OpenCode | |
| OpenCode coding agent | |
| Qwen: download, install, fine-tune and use |