Lesson 4: Building a multi-skill plugin
30 mingit-hygiene plugin that bundles your commit-conventions skill into a versioned, installable library.Two ways through this lesson: read it on this page, or run it hands-on in your coding agent. To do it in your agent:
npx tessl install tessl-academy/skill-foundationsRun this once, in a fresh project directory (for example a new foundations folder, because Tessl won't initialize in your home directory). It installs the skills your agent uses to guide you through the lessons interactively.“guide me through building a multi-skill plugin”Open your coding agent (Claude Code, Cursor, Codex, or Tessl Agent) in that directory and ask it the prompt above. The installed skill picks it up and walks you through this lesson step by step. Prefer a command? Launch it directly with tessl launch skill --agent claude-code -i 04-building-a-multi-skill-plugin (swap claude-code for cursor, codex, or tessl-agent).Until now you've worked with one skill at a time: you installed commit-conventions, then you wrote your own. But a single skill rarely travels alone. The moment you have a second skill that serves the same people for the same kind of work, you want to ship them together: install once, version once, update once. That bundle is a plugin, and this lesson is about building one that holds a small, coherent library of skills rather than a pile of unrelated ones.
What you'll build
You'll turn your commit-conventions skill into the first member of a git-hygiene plugin, a library for clean git workflows that could also hold pr-description and release-notes. By the end you'll have:
- A plugin scaffolded with the Tessl CLI, versioned, with its
skills/folder waiting. - Your
commit-conventionsskill added to it and the whole plugin linting clean. - A working rule for deciding what belongs in the plugin, and how to keep its skills from stepping on each other.
A plugin is the unit you ship
You've already been using plugins without dwelling on them: every skill you installed arrived inside one. Worth making the distinction explicit, because it shapes everything below: the unit of authorship is the skill; the unit of distribution is the plugin. A plugin is what gets versioned, published, and installed, and it can bundle:
- One skill: the simple case, one
SKILL.md, one purpose, one version. - Multiple related skills: a library of sibling skills that share an audience and a release cadence.
- Skills, rules, and docs: a richer context bundle for a problem space that needs more than skills alone.
Plugins are agent-agnostic: the same my-workspace/git-hygiene@1.0.0 installs into Claude Code, Cursor, Codex, or Gemini in whichever format each expects. You author once; Tessl handles the per-agent sync on install.
What makes skills belong in the same plugin
Before bundling anything, the judgment call: a plugin is not "everything one team has ever written." It's a coherent group of skills that share:
- An audience: the same people install all of them.
- A problem space: they help with adjacent jobs, not unrelated ones.
- A release rhythm: when one needs updating, the others usually do too.
The git-hygiene family is the clean example: commit-conventions, pr-description, release-notes. Each takes git context and produces text a human needs to read. Same audience, same problem space, same likely update cadence. One plugin.
Heuristic. If skill B's release cycle would always lag skill A's by weeks, they probably belong in separate plugins. Versioning skills together means versioning them together: every change to one bumps the version for all. Bundling is a commitment, not a convenience.
Before you start
This builds directly on the skill you authored in Lesson 2, so you already have what you need: the Tessl CLI, a workspace to publish into (the namespace your plugins belong to, where your personal one is fine), and your commit-conventions skill on disk. A quick reminder of the shape of a skill, since you'll be moving one around: a skill is a SKILL.md whose description frontmatter decides when the agent loads it and whose body tells it how. Hold onto that description idea; it comes back at the end when the library grows.
What a plugin looks like on disk
A plugin is mostly convention, so a quick look at one tells you the whole structure. It's a manifest plus a directory of skills:
git-hygiene/
├── .tessl-plugin/
│ └── plugin.json # versioned package metadata
└── skills/
└── commit-conventions/
├── SKILL.md # the contract: when to use, how to do it
└── examples/
The .tessl-plugin/plugin.json carries the package metadata and points at the skills directory:
{
"name": "my-workspace/git-hygiene",
"version": "0.1.0",
"description": "Skills for clean git workflows: commits, PRs, release notes",
"private": true,
"skills": "skills"
}
The field that makes multi-skill plugins painless is skills. It's a path, not a list: Tessl scans that directory for any subdirectory containing a SKILL.md and discovers each skill by convention. You never hand-maintain a roster of skills; you drop a skill directory in and it's part of the plugin. Leave the field out altogether and Tessl uses skills anyway, which is why the manifest the CLI writes for you doesn't carry it: you only name the path to point somewhere else. (private: true keeps it to your workspace; a rules field works the same way for always-loaded context.)
Step-by-step
1. Scaffold the plugin
For a library that bundles more than one skill, scaffold the plugin directly rather than through tessl skill new, which assumes a single-skill plugin. One constraint to know first: the CLI won't create a plugin with nothing in it. Run it without a skill or a rules file and it stops with ✘ Must specify at least one of: --skill or --rules. A git-hygiene plugin wants a rules file anyway, so start there and leave skills/ for the skill you already wrote:
tessl plugin new \
--name my-workspace/git-hygiene \
--summary "Skills for clean git workflows: commits, PRs, release notes" \
--path ./plugins/git-hygiene \
--workspace my-workspace \
--rules git-hygiene
You'll get a plugins/git-hygiene/ holding .tessl-plugin/plugin.json and a rules/git-hygiene.md waiting for instructions. There's no skills/ directory yet, and the manifest has four fields with no skills path among them. Both are the convention at work: you make the directory in the next step, and Tessl finds it without being told. The version starts at 0.1.0, semver in the form MAJOR.MINOR.PATCH. Keep it in the 0.x range while you're iterating; bump to 1.0.0 when the shape is stable enough for other teams to depend on it.
2. Add your skill to it
Make the skills/ directory the scaffold left out, and copy in the commit-conventions skill you built in Lesson 2. Mind what you copy: tessl skill new --path ./skills/commit-conventions created a single-skill plugin at that path, and the skill itself sits one level inside it. Copy the outer directory and you nest a plugin inside a plugin, so take the inner one:
mkdir -p ./plugins/git-hygiene/skills
cp -r ./skills/commit-conventions/skills/commit-conventions \
./plugins/git-hygiene/skills/commit-conventions
You want plugins/git-hygiene/skills/commit-conventions/SKILL.md, three levels deep and not four. A skills/ directory inside your skill directory means you copied the wrapper.
That's all the wiring it needs, with no manifest edit. Because the manifest names no skills path, Tessl falls back to scanning skills/, so the skill is part of the plugin the moment its directory lands there. Adding pr-description and release-notes later is the same move: copy the directory in, done.
Lint the whole plugin after each addition to catch structural mistakes early:
tessl plugin lint ./plugins/git-hygiene
Plugin lint is skill lint applied across every skill the plugin holds, plus checks that the plugin-level fields (name, version, description) are present and that the skills it discovers resolve to real skill directories. Fix anything it flags and re-run until it's clean.
The thing that breaks libraries: overlapping descriptions
A library introduces a problem a single skill never has. When two skills' descriptions overlap, the agent gets confused about which one to load, and that overlap is the most common reason a library underperforms in practice.
Each description should name a situation specific enough that no other skill in the same workspace could plausibly match it:
commit-conventions: "the user wants a commit message"pr-description: "the user is opening a PR and wants a description"release-notes: "the user is preparing a release and wants notes between tags"
Three skills, three distinct user situations, no ambiguity about which fires when. A quick diagnostic: if you can read two of your descriptions and imagine the same user prompt matching both, they overlap. Tighten the narrower one, usually by naming the trigger phrase the user would actually say rather than the general topic, until the overlap is gone.
The mental model. A library's quality lives in its descriptions. The skills can each be excellent on their own, but if the agent can't tell them apart, the library is worse than the sum of its parts.
What you keep
There's a git-hygiene plugin on your disk now, with commit-conventions inside it and a manifest that will keep discovering skills as you add them. You also have the test for whether bundling is the right call: shared audience, shared problem space, shared cadence. And you know what goes wrong as a library grows, which is descriptions that blur into each other until the agent can't tell which skill to load. Next you publish this plugin, so your team installs it the way you installed commit-conventions in Lesson 1.