Skill Foundations · Lesson 2

Lesson 2 — Writing your first skill

30 min
What you keep A commit-conventions skill you authored, linted, and installed yourself.

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:

1 · Install once per course npx tessl install tessl-academy/skill-foundations Run this once, in a fresh project directory (for example a new foundations folder — Tessl won't initialize in your home directory). It installs the skills your agent uses to guide you through the lessons interactively.
2 · Start the lesson — ask your agent “guide me through writing my first skill” 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 02-writing-your-first-skill (swap claude-code for cursor, codex, or tessl-agent).

In the last lesson you installed someone else's skill and watched it work. This time you write your own: a commit-conventions skill that turns a staged diff into a Conventional Commits message. You take it all the way from an empty directory to something your agent loads and fires on intent. It's the same skill you've already seen in action, which is the point: you know what "good" looks like, so you can focus on building it rather than wondering what you're aiming for.

What you'll build

By the end you'll have:

  • A skill directory you scaffolded with the Tessl CLI, with a SKILL.md you wrote yourself.
  • A clean lint pass against the Agent Skills specification.
  • The skill installed straight from your filesystem, firing when you ask for a commit message and staying quiet when you don't.

What a skill is, quickly

A quick recap, because authoring one means knowing its parts. A skill is three things stitched together:

  1. A SKILL.md file, markdown with frontmatter. The frontmatter tells the agent when to load the skill (the description field); the body tells it how to do the job. This file is the contract.
  2. Optional supporting files: examples, helper scripts, reference data. Anything the SKILL.md references.
  3. A package wrapper, which in Tessl is a plugin. It gives the skill a name (workspace/plugin-name), a version, and a place in the registry so anyone can install it.

The mental model. Think of skills as the agent equivalent of small, well-tested libraries: each does one thing, and your agent picks the right one for the job at hand.

You'll write the first part by hand and let the CLI generate the wrapper. So before touching the keyboard, it's worth knowing the two parts of SKILL.md that decide whether your skill is any good.

The two parts that matter

The description is the whole trigger

The description in the frontmatter matters more than anything else in the file. It's the only thing the agent sees when deciding whether to load your skill on a given turn. Once loaded, the body takes over, but if the description doesn't match what the user is doing, the body never runs.

Write it as two things stitched together:

  1. A trigger condition: what the user is doing when this skill should fire. "Use when the user asks for a commit message, or has staged changes…"
  2. A capability statement: what the skill does in response. "Generates a Conventional Commits–formatted message from a diff."

Be specific. "Helps with git" is too vague to ever fire, because the agent can't tell it apart from a dozen similar skills. Name the user-visible situation that should trigger it, then name the artifact it produces. Get the description right and most other things forgive themselves.

The body briefs the agent, not the user

The description is for finding the skill; the body is for using it. Write it as if briefing a competent colleague who already knows the domain, not a tutorial for the user and not documentation. The agent reads this; the user typically never sees it. A consistent shape makes skills easier to author and review:

  • Numbered steps for actions taken in sequence.
  • Decision tables for classification problems (like a Conventional Commits type table).
  • Constraint lists for hard rules: subject-line length, capitalization, what not to include.
  • Explicit references to examples/ when they exist.

What you leave out matters as much. Skip explanations of what the skill does (the description said that), context the agent can read directly (the staged diff, which you shouldn't restate), hedging ("you might want to"), and padding.

Body length is a context budget, not a style preference. Every line is loaded into the window on every matching turn. A tight body that says the right thing beats a thorough one that crowds out everything else. Past roughly 400 lines, split the skill before extending it.

Before you start

You need three things on your machine:

  • A coding agent. The examples assume Claude Code or Cursor, but skills are agent-agnostic, so Codex and Gemini work too.
  • The Tessl CLI. Follow the installation guide (Homebrew, winget, or the install script), then run tessl login.
  • A repository to work in, initialized with Tessl. If you haven't yet:
tessl init --agent claude-code
# or: tessl init --agent cursor

That's all you need to author and install a skill locally. Publishing to a shared workspace (the namespace skills live under, workspace/plugin-name) comes later, in the publishing lesson; for now everything stays on your own machine.

Step-by-step

1. Scaffold the skill

The CLI gives you a wizard that creates a well-formed skill directory in one shot:

tessl skill new \
  --name commit-conventions \
  --description "Use when the user asks for a commit message or wants help summarizing staged changes. Generates a Conventional Commits–formatted message from a diff." \
  --path ./skills/commit-conventions

You'll get a directory like this:

skills/commit-conventions/          ← plugin root (your --path)
├── .tessl-plugin/
│   └── plugin.json                    # plugin metadata
└── skills/
    └── commit-conventions/         ← skill directory (your --name)
        └── SKILL.md                   # your skill file, stubbed and ready to edit

The SKILL.md you'll edit is nested inside the plugin's own skills/ directory. The .tessl-plugin/plugin.json points its skills path at that directory, and Tessl discovers the skill inside it by convention; you don't list skills by hand.

2. Write the SKILL.md

Open the stubbed SKILL.md and make it the real thing. Here's a complete, lint-clean version. Read each section before you paste it, because the point of authoring this is understanding why each line is there.

---
name: commit-conventions
description: Use when the user asks for a commit message or wants help summarizing staged changes. Generates a Conventional Commits–formatted message from a diff.
---

# Commit Conventions

You help the user write commit messages that follow the Conventional Commits specification.

## When you're triggered

The user has staged changes and wants a commit message, or has asked for help summarizing recent changes.

## What to do

1. Read the staged diff (`git diff --cached`) if you haven't already.
2. Identify the dominant change type. Use the table below.
3. Compose a message of the form `<type>(<optional scope>): <imperative subject>`.
4. Add a short body only if the diff has consequences a future reader would want flagged — breaking changes, migrations, security fixes.
5. Show the user the message before they commit. Don't run `git commit` unless they ask.

## Type table

| Type | When |
|---|---|
| `feat` | New user-visible capability. |
| `fix` | Bug fix the user can perceive. |
| `refactor` | Internal restructure with no behavior change. |
| `docs` | Documentation only. |
| `test` | Tests only. |
| `chore` | Tooling, deps, build, CI — anything below the user's perception. |
| `perf` | Measurable performance improvement. |

## Subject line rules

- Imperative mood ("add", not "added" or "adds").
- Lowercase first letter, no trailing period.
- Hard limit 72 characters.
- Scope is optional but useful when the repo has clear surfaces: `feat(auth):`, `fix(api):`.

## Examples

See `examples/` for diff → message pairs you can use to ground your reasoning.

Notice how the body lives by the conventions above: the description is a trigger condition plus a capability statement, the steps are numbered, the classification problem is a table, and the rules are a tight constraint list. Nothing explains "what this skill is for"; the description already did.

3. Lint it

Before you install or publish, run the linter:

tessl skill lint ./skills/commit-conventions

It validates structure: the plugin root has a valid .tessl-plugin/plugin.json, the required SKILL.md frontmatter fields are present, every file referenced from SKILL.md exists, and the directory matches the Agent Skills specification. Fix anything it flags and re-run until it passes.

Lint checks structure, not quality. A clean pass means your skill is structurally sound. Whether the description is specific enough to fire reliably is a different question, answered by tessl review run. Whether the skill behaves well is a third, answered by evals. You'll meet both in later lessons.

4. Install it from your filesystem

You don't have to publish to try it. Install straight from the local path:

tessl install file:./skills/commit-conventions

This is the authoring loop: edit SKILL.md, reinstall, try it in your agent, edit again. To skip the manual reinstall, run tessl install --watch-local on its own in a second terminal — with no source it watches every local file-source skill already installed and re-syncs each time you save:

tessl install --watch-local

Local installs behave exactly like registry installs once synced: they land in .tessl/plugins/ and the agent picks them up through the same mechanism. The only difference is the source.

5. Trigger the skill you just authored

You installed the skill straight from your filesystem, so this is the moment it earns its keep. Stage a real change:

echo "# Try Skills" > README.md
git add README.md

Then ask your agent, in Claude Code, Cursor's chat, or whatever you're using:

"What's a good commit message for the staged changes?"

The agent should recognize that the prompt matches your skill's description, load the SKILL.md you wrote, read the staged diff, and propose something like:

docs: add initial README

Adds a placeholder README to seed the repo.

Try staging different changes (a new function, a bug fix, a dependency bump) and ask again. Watch the prefix change: feat:, fix:, chore:, refactor:. That's your skill doing its job.

Edit and re-trigger. Because the skill came from a local path, you can tweak the SKILL.md, reinstall (or let --watch-local re-sync for you), and trigger it again. The change shows up on the next prompt. That tight loop is the whole point of installing from the filesystem before you publish.

Verify it works

Four quick checks:

  • tessl skill lint ./skills/commit-conventions returns a clean pass.
  • tessl list shows your commit-conventions plugin installed alongside any other skills you have.
  • A "give me a commit message" prompt triggers your skill, not someone else's. If you still have the previous lesson's commit-conventions installed, uninstall it first so the trigger isn't ambiguous.
  • An unrelated prompt, like "explain this function", does not load the skill.

If your skill doesn't trigger and no older version is shadowing it, the usual cause is a too-narrow description. Make sure the words a user would actually say, like "commit", "message", and "staged", appear somewhere in the description field.

What you keep

You walk away with a commit-conventions skill you wrote, linted, and installed yourself, and, more usefully, the instinct behind it: a sharp description so the agent knows when to reach for the skill, and a tight body that tells it how without crowding the context window. That instinct is what every authoring task after this builds on. Next, you'll put a skill under test, so you can change it with confidence instead of hope.

Lesson 2 complete ✓