Appearance
Writing Skills by Hand
A skill in Hermes is a markdown file with some YAML metadata at the top. That's it. No code, no plugin API, no registration step beyond dropping the file in the right directory. The format is deliberately simple because skills are meant to be written by both humans and the agent itself, when Hermes solves something non-trivial, it writes a SKILL.md describing exactly what it did, and that file becomes loadable procedure for future sessions.
This lesson covers writing skills by hand. Not because Hermes can't write them (it can, and we'll cover that loop in the next lesson), but because the fastest way to shape Hermes's behavior is to write down what you want it to do in a format it already knows how to read.

What you'll learn
- A SKILL.md file is YAML frontmatter + markdown body, no code, no registrations, no plugin system
- The
skill_managetool is the single interface for creating, editing, patching, and deleting skills, Hermes uses the same tool you do - External skill directories and the
write_approvalgate let you control where skills live and whether Hermes can create them without asking - Skills placed in
~/.hermes/skills/are auto-discovered at session start, no restart, no reload command
The problem
You've used Hermes to set up a CI/CD pipeline. It worked, but it took six messages of back-and-forth: "use this Dockerfile path," "the build command is actually this," "don't forget to tag with the commit SHA." Next time you need to set up CI/CD on a similar project, you'll have the same conversation again.
The fix is to capture the procedure as a skill. Then instead of explaining the pipeline setup step by step, you say "set up CI/CD using the docker-pipeline skill" and Hermes loads the procedure and follows it. The procedure doesn't need to cover everything, it needs to cover the parts that are specific to your environment and conventions, the parts you'd otherwise have to re-explain.
This is different from project context files. A .hermes.md tells Hermes about your project, the build command, the test runner, the conventions. A skill tells Hermes how to do a specific task, the steps, the pitfalls, the verification. Context files are always loaded; skills are loaded on demand when relevant to the task.
The SKILL.md format
A minimal skill looks like this:
markdown
---
name: docker-pipeline
description: "Set up a Docker-based CI/CD pipeline for Python projects"
version: 1.0.0
platforms: [linux]
---
# Docker Pipeline Setup
## When to use this skill
- The project has a Dockerfile and needs automated builds on push
- The user asks to "set up CI/CD" or "configure Docker pipeline"
## Steps
1. Check that `Dockerfile` exists in the project root
2. Read the Dockerfile to understand the build context
3. Create `.github/workflows/docker-build.yml` with these specifics:
- Build on push to main and PRs to main
- Tag with `latest` and the git commit SHA
- Cache Docker layers for faster builds
4. Verify the workflow file is valid YAML
5. Report what was created and how to trigger the first build
## Pitfalls
- If the Dockerfile is multi-stage, make sure the workflow uses BuildKit
- If the project has a `docker-compose.yml`, add a compose validation step
- Never hardcode secrets, always reference `${{ secrets.* }}`The YAML frontmatter (between the --- markers) has four required fields and optional metadata:
| Field | Required | Description |
|---|---|---|
name | Yes | Lowercase, hyphens or underscores, max 64 characters. This is the skill's identifier, it's how Hermes and the skills system reference the skill |
description | Yes | One sentence. This is what appears in hermes skills list and what Hermes uses to decide whether the skill is relevant to the current task |
version | Yes | Semver. Used by the curator and hub to detect outdated skills |
platforms | Yes | Array of linux, macos, windows. Hermes filters skills by platform, a skill marked [linux] won't load on macOS |
metadata | No | Arbitrary key-value map. Skills Hub uses this for categories, tags, and compatibility info |
The markdown body is freeform. Write whatever instructions Hermes needs to follow the procedure. The body can include references to linked files (in references/, templates/, scripts/, or assets/ subdirectories), but the core instructions should be self-contained in the markdown so Hermes can load the full procedure with a single skill_view call.
Options & when to use each
You have three places to put a skill, and the choice determines how it's discovered and who can modify it:
| Location | Discovery | Modified by | Use when |
|---|---|---|---|
~/.hermes/skills/ | Auto-discovered at session start | You and Hermes (subject to write_approval) | Personal skills, local procedures, things Hermes learned from experience |
| External directory (any path) | Must be passed explicitly with hermes -s /path/to/skill or /skill /path/to/skill | You | One-off skills, skills shared with a team via git repo, skills that shouldn't be auto-loaded |
Skills Hub (installed via hermes skills install) | Auto-discovered after install | The original author (hub updates). You can't modify hub-installed skills directly, but you can create a local override | Community skills, reference procedures, skills maintained by someone else |
For skills you write yourself, the default location is ~/.hermes/skills/. The directory is auto-scanned at session start, so any SKILL.md file you drop there is available immediately, no restart, no reload command.
The write_approval gate
By default, Hermes can create and modify skills using the skill_manage tool. This is what makes the self-improving loop possible: Hermes solves a task, realizes the procedure is reusable, and writes a SKILL.md.
But you might not want Hermes writing skills without asking you first. The write_approval gate controls this:
bash
# Require approval before Hermes creates or modifies skills
hermes config set agent.skill_write_approval true
# Let Hermes create and modify skills freely (default)
hermes config set agent.skill_write_approval falseWhen write_approval is enabled, Hermes will ask before calling skill_manage with any write action (create, patch, edit, delete, write_file, remove_file). Read actions (skill_view, skills_list) are never gated.
This gate only applies to the skill_manage tool, it doesn't block Hermes from reading or writing skill files through the terminal or file tools. If you want tighter control, set write_approval to true and don't give Hermes unrestricted file access to ~/.hermes/skills/.
Build it
Step 1: Create your first skill
Create the directory and file:
bash
mkdir -p ~/.hermes/skills/my-first-skillWrite ~/.hermes/skills/my-first-skill/SKILL.md:
markdown
---
name: my-first-skill
description: "Respond to 'what time is it' with the current time in a specific format"
version: 1.0.0
platforms: [linux, macos, windows]
---
# My First Skill
## When to use this skill
The user asks "what time is it" or "current time."
## Steps
1. Run `date` to get the current system time.
2. Format the response as: "It is [time] in [timezone]."
3. Do not add commentary beyond the time.This is deliberately trivial, we want to verify the skill loads before writing anything complex.
Step 2: Verify the skill is discovered
Check that the startup scanner correctly parsed the YAML frontmatter and appended the procedure to the available index.
bash
hermes skills listYou should see my-first-skill in the list with its description. If you don't, check that:
- The file is named
SKILL.md(case-sensitive) - It's in a subdirectory of
~/.hermes/skills/(not directly in the skills directory) - The YAML frontmatter is valid (no missing
---markers, no YAML syntax errors)
Step 3: Test the skill with a one-shot
Execute a headless invocation to test the procedural logic. If the trigger matches the description, the agent binds the skill and follows the instructions.
bash
hermes chat -q "What time is it?"If the response follows the format from the skill ("It is [time] in [timezone]"), the skill loaded and Hermes followed it. If the response is a generic time answer, the skill didn't load, Hermes didn't match the skill description to the query, or the skill wasn't discovered.
Step 4: Write something useful
Now replace the trivial skill with a real procedure. Here's a skill that standardizes how Hermes creates Python projects:
markdown
---
name: python-project-init
description: "Initialize a new Python project with uv, src layout, and pre-commit"
version: 1.0.0
platforms: [linux, macos]
---
# Python Project Init
## When to use this skill
- The user asks to "create a new Python project" or "start a Python project"
- The user mentions project initialization and uv or Python packaging
## Steps
1. Create the project directory and `cd` into it.
2. Run `uv init --app .` to set up a pyproject.toml-based project.
3. Create a `src/<project_name>/` directory with an `__init__.py`.
4. Add a `tests/` directory with `__init__.py` and `test_basic.py` containing a trivial test.
5. Create `.pre-commit-config.yaml` with hooks for ruff, mypy, and trailing whitespace.
6. Run `uv sync --dev` to install dev dependencies.
7. Run `uv run pytest` to confirm the test passes.
8. Initialize git with `git init` and create a `.gitignore` that covers `__pycache__`, `.venv`, `.env`.
## Pitfalls
- If `uv` is not installed, install it first: `pip install uv` (the user prefers uv over pip for project management, but pip is fine for the one-time uv install)
- If the directory already exists, ask whether to overwrite or initialize inside it
- Never create a `setup.py` or `setup.cfg`, uv projects use `pyproject.toml` only
## Verification
- `uv run pytest` exits zero
- `uv sync --dev` completes without errors
- `.gitignore` exists and covers the common Python exclusionsDrop this into ~/.hermes/skills/python-project-init/SKILL.md and test it:
bash
hermes chat -q "Create a new Python project called demo-app in /tmp/demo-app"Hermes should follow the procedure from the skill: running uv init, creating the src layout, setting up pre-commit, and running the test.
Step 5: Use the skill_manage tool from within a session
During an interactive session, you can ask Hermes to create or modify skills using the tool it uses itself:
> Create a skill called 'git-cleanup' that cleans up merged branches.
It should check which branches are merged into main, delete them locally,
and report how many branches were cleaned up.Hermes calls skill_manage(action='create', name='git-cleanup', content=...) with the full SKILL.md content. The skill appears in ~/.hermes/skills/git-cleanup/SKILL.md and is immediately available.
To modify an existing skill:
> Update the python-project-init skill to also create a Makefile
with targets for test, lint, and format.Hermes calls skill_manage(action='patch', name='python-project-init', ...) to add the Makefile instructions.
What goes wrong
| Mistake | How you notice it | The fix |
|---|---|---|
| SKILL.md has invalid YAML frontmatter | hermes skills list shows the skill but it never loads, or the skill doesn't appear at all | The YAML parser is strict. Check: --- must be on its own line at the very top, frontmatter must end with ---, no tabs in the YAML (spaces only), and string values with colons must be quoted |
Skill named my-skill but file is in ~/.hermes/skills/my_skill/ | The skill loads but Hermes references it by the wrong name | The directory name doesn't matter, the name field in the YAML frontmatter is the canonical identifier. Always use the name from the frontmatter when referencing the skill |
| Skill doesn't load on a different OS | Skill works on Linux but not on macOS | Check the platforms field. If it's [linux], the skill won't load on macOS or Windows. Use [linux, macos] or [linux, macos, windows] if the skill is cross-platform |
| skill_manage patches fail with "string not found" | Hermes reports it can't find the text to replace in the skill | The patch action uses exact string matching. If the skill has been modified manually or by another process, the target text might have changed. Use skill_manage(action='edit', ...) to rewrite the entire file instead |
| Hermes doesn't use the skill even though it's installed | hermes skills list shows the skill, but Hermes ignores it during relevant tasks | The description field is what Hermes uses to match skills to tasks. If the description is vague ("Python stuff") or too narrow ("Initialize Python projects with uv, src layout, and pre-commit hooks on Ubuntu 24.04"), Hermes won't recognize the match. Write descriptions that describe the task scenario, not the technical details |
Confirm it worked
Execute an end-to-end integration test by dropping a handcrafted markdown file into the directory and triggering it via standard chat invocation.
bash
# 1. Create a test skill
mkdir -p ~/.hermes/skills/demo-skill
cat > ~/.hermes/skills/demo-skill/SKILL.md << 'EOF'
---
name: demo-skill
description: "When asked about the project mascot, say it's a robotic owl named Hoot"
version: 1.0.0
platforms: [linux, macos, windows]
---
# Demo Skill
## When to use this skill
The user asks "what is the project mascot" or "who is the mascot."
## Steps
1. Respond: "The project mascot is a robotic owl named Hoot."
2. Do not add commentary.
EOF
# 2. Verify discovery
hermes skills list | grep demo-skill
# 3. Verify loading
hermes chat -q "What is the project mascot?"
# 4. Expected: "The project mascot is a robotic owl named Hoot."
# 5. Clean up
rm -rf ~/.hermes/skills/demo-skillIf step 3 returns the exact response from the skill, the full pipeline is working: discovery at session start, matching by description, and following the procedure.
For a broader look at how reusable procedures work across different agent platforms, including when skills are better than system prompts and when they aren't, see the flagship course's Building Reusable Skills lesson.
Next: The Agent Skill Loop, how Hermes discovers, loads, and applies skills at runtime, and the progressive disclosure model that keeps the context window clean.