DEV Community

Krish Verma
Krish Verma

Posted on

How I built the skills system in my AI assistant: markdown files, strict validation, zero code

Ankita, my open-source desktop AI assistant, has a growing number of repeatable workflows: reviewing commits, drafting release notes, doing structured web research, reproducing bugs. For a long time each of those was just a paragraph buried in my notes, copy-pasted whenever I needed it.

So I built a skills system. A skill is not code. It's a folder with a markdown file. That's the whole idea, and it's held up better than anything clever I considered.

What a skill actually is

Every skill lives in a skills/ directory at the repo root as a single folder containing a SKILL.md file. The folder is the name — enforced, not suggested. The markdown file has a small frontmatter block:

---
name: release-notes
description: "Draft release notes from git history and the changelog. Use when cutting a release or summarizing what changed."
suggested-tools: git, read_file
---
# Release Notes

1. Call `find_tools("git")` if the git tool is not available yet.
2. Get the range: `git log` since the last release tag...
Enter fullscreen mode Exit fullscreen mode

The body is step-by-step instructions the agent follows. Nothing executes. A skill is a procedure, not a plugin — and that distinction turned out to be the most important design decision.

Progressive disclosure, or: five lines in every prompt

Ankita currently ships five built-in skills: commit-review, release-notes, web-research, bug-repro, and ankita-dev. If all five full documents were injected into every prompt, context would bloat fast. So only one line per skill goes into the system prompt — the name and its description, plus the call syntax:

Available skills (interactive chats, progressive disclosure):
  - release-notes: Draft release notes from git history and the changelog...
    Suggested tools: git, read_file. To use, call skill({"name":"release-notes"}).
Enter fullscreen mode Exit fullscreen mode

The full body loads on demand through a skill tool, capped at 8000 characters of output. The description line is the advertisement; the body is the product. This is the same "deferred" philosophy behind my tool discovery system, applied to instructions instead of schemas: pay for what you use, nothing else.

Skills teach; they never fetch

The skill tool's own description says it plainly: "Returns markdown to follow; suggested tools are hints only." The prompt lines echo it: "Skills are instructions only. Suggested tools are hints, not requirements. Use find_tools to load tools if needed."

That separation matters. The skill tells the agent what to do (read the changelog, group changes by type, credit contributors). It never touches tools itself. When the agent needs the git tool, it loads it through the normal deferred tool-discovery path, with all the same approval and validation rules as everything else. Skills stay decoupled from the tool catalogue, which means I can add a tool without updating any skill, and write a skill without knowing the full tool list.

It also keeps the trust boundary simple. Skill markdown is data the agent reads. It is not code that runs. There's no execution surface in the skill system at all.

Validation so one bad file can't break anything

Because skills are just files on disk, I treat them as untrusted input — including my own. The parser (src/core/skills.mjs) validates everything:

  • The folder name must match the frontmatter name and pass ^[a-z0-9-]{1,64}$.
  • The description must be 10–300 characters.
  • The body must be non-empty and under 12,000 characters on disk.
  • Suggested tools, if present, max 200 characters.

A skill that fails any of this is skipped with a logged error. It never throws, never breaks startup, never poisons the prompt. Graceful degradation for config files is one of those things that's boring to write and embarrassing to need — do it first.

Skills are also user-toggleable. There's a Plugins > Skills screen in the desktop app, and the agent filters disabled skills out of the prompt entirely. Calling a disabled skill returns a plain error string, not a stack trace.

One more detail: each skill folder can optionally include a plugin.json contributing "palette" actions (quick commands in the UI), validated through a palette schema. Instructions and UI actions, one folder.

What I'd do differently

Two things. First, loading is cached with file-stamp keys and a reloadSkills() escape hatch — fine, but the invalidation story got more complex than the feature deserved. Second, I wish I'd defined the "skill vs. tool vs. prompt" boundary in writing earlier. My rule now: tools do, skills teach, the system prompt frames. If something feels like it belongs in two of those, it belongs in neither until I rethink it.

The point

The skills system is maybe 150 lines of parsing, caching, and one tool wrapper. It has no execution engine, no sandboxing problem, no dependency graph — because a skill is just markdown the agent chooses to read. Constraints made it better, not smaller.

If you're building an agent and reaching for a plugin SDK, consider whether what you actually need is a well-structured text file. It was, for me.


Ankita is an open-source desktop AI assistant I'm building — the code is at github.com/akyourowngames/A.N.K.I.T.A. If you've built something similar (skills, prompt packages, agent playbooks), I'd genuinely like to hear how you drew the line between instructions and code — drop a comment.

Top comments (0)