Skip to content
Open Console

Add skills to your agent

5 min readUpdated

A skill is a folder with a SKILL.md in it: a name, a description, and instructions for one job. The model reads only the names and descriptions up front, and pulls the full instructions in when a request calls for that job. That is what lets you ship a dozen detailed procedures without spending a dozen procedures’ worth of context on every run.

Put the folder in your agent, deploy, and the platform delivers it to the harness.

Your agent bundle
my-agent/
├── agent.py
├── agent.yaml
├── pyproject.toml
└── skills/
└── release-notes/
└── SKILL.md

The directory name is the skill name, and it has to match the name in the frontmatter:

skills/release-notes/SKILL.md
---
name: release-notes
description: Turn a list of merged pull requests into release notes grouped by change type. Use when asked for release notes, a changelog, or a summary of what shipped.
---
# Release notes
Turn merged pull requests into notes a reader can skim.
## Steps
1. Group the PRs by type: Added, Changed, Fixed, Removed. Drop anything purely
internal unless the user asks for the full list.
2. Write one line per entry, starting with a past-tense verb. Say what changed for
the reader, not which files moved.
3. Put breaking changes first, under their own **Breaking** heading.
## Output
Markdown, one `##` heading per group. No preamble and no closing summary.

description is the only thing the model sees before deciding whether to open the skill, so it carries the whole routing decision. Say what the skill does and when to reach for it. “Release notes helper” gives the model nothing to match a request against; the description above names the words a user would actually type.

Anything else in the folder (reference files, scripts, templates) ships with it, and the model can read those once the skill is open.

Names are restricted to letters, digits, ., _ and -. A name outside that set fails the run, because skill names are passed through to the harness as tool-permission entries.

Nothing to declare in the manifest. The skills/ directory’s existence is the declaration:

Terminal
tanomis agent deploy . --tenant acme-prod

Your agent’s code is baked in at /agent, and on each run the platform mirrors /agent/skills/ into the session workspace at <workspace>/.claude/skills/. That copy is what the harness reads, because a run cannot read your image directly: the agent’s working directory is the session workspace, and paths outside it are refused.

Two bounds are enforced before anything is written: 500 files and 5 MB total. Over either one, the run fails and names the cap it hit. It fails rather than truncating on purpose, because a half-present skill tree is the kind of problem that only shows up the first time someone needs the skill.

Shipping the folder is the whole job. Project scope is on by default, so the CLI discovers the mirror and the model can use the skills on the next run.

To narrow which of your shipped skills are available on a given run, set skills:

agent.py
def create_agent(input_payload, context):
return (
input_payload.get("prompt", ""),
{
"system_prompt": "You write release notes.",
"skills": ["release-notes"], # or "all"
},
)

Options can be a plain dict, which keeps your module importable without the runtime. See Claude Agent SDK for the full authoring shape.

Changing skills partway through a session is rejected, because it reshapes the conversation. Start a new session to change the set.

Frontmatter that triggers an approval prompt

Section titled “Frontmatter that triggers an approval prompt”

Keep your frontmatter to name and description unless you need more. A SKILL.md that declares allowed-tools, disallowed-tools, hooks, or an execution context is treated by the CLI as a skill that needs confirmation, so every invocation raises a human-in-the-loop approval instead of running straight through. In an unattended run that surfaces as a denial rather than a pause.

If you need one of those fields, either expect the prompt or name the skill explicitly in skills, which registers it as pre-allowed.

You compile your own graph, so the wiring is yours. Pass both backend= and skills= to create_deep_agent:

agent.py
def create_agent(input_payload, context):
from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend
backend = FilesystemBackend(
root_dir=str(context.workspace_root),
virtual_mode=True,
)
skills_root = context.workspace_root / ".claude" / "skills"
return create_deep_agent(
model=context.runtime_context.model,
tools=[...],
backend=backend,
skills=["/.claude/skills/"] if skills_root.is_dir() else None,
)

skills takes backend paths, not filesystem paths, which is why the backend is rooted at the workspace and the source is the path within it. virtual_mode=True is what makes /.claude/skills/... resolve as a backend path; it is a path-mapping mode rather than a sandbox, so treat it as wiring, not as a containment control.

The is_dir() guard keeps the agent working when there is no skills/ directory, during local development or if you remove it later.

Every run with skills present emits an agent_skills_loaded event naming what was found, so “did my skill actually ship?” is answerable from the run’s event stream:

{
"type": "agent_skills_loaded",
"source": "agent_service",
"data": {
"skills": ["release-notes"],
"file_count": 1,
"size_bytes": 742,
"changed": true,
"warnings": []
}
}

skills lists only the directories that can actually produce a skill. A directory with no SKILL.md, no frontmatter, no description, or a name that does not match its directory is left out and reported in warnings instead, because a harness would drop it and the event should not claim otherwise. SKILL.md is case-sensitive.

If the event is missing entirely, the agent shipped without a skills/ directory. Check that it sits beside agent.yaml and that your build did not exclude it.

Skills are instructions, not capabilities. A skill can tell the model how to use a tool, but it cannot grant access to one. What the agent may call is decided by policy, by the connectors attached to the version, and by the authorization ceiling, none of which a skill can widen.

That is worth keeping in mind when you write one: a skill that says “call the GitHub API” does nothing unless a connector supplying that tool is attached.