# Add skills to your agent

Ship a folder of SKILL.md files with your agent so the model loads the right procedure on demand, on the Claude Agent SDK and Deep Agents harnesses.

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.

- Skills live in `skills/` next to your `agent.yaml`, and ship inside your agent's image.
- The platform mirrors them into the session workspace at `.claude/skills/` on every run.
- **Claude Agent SDK:** shipping the folder is enough.
- **Deep Agents:** pass `backend=` and `skills=` to `create_deep_agent`.
- The mirror is platform-managed. The image is the source of truth, and anything written
  into the mirror at run time is replaced on the next run.

## Write a skill

```text title="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:

```markdown title="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.

## Ship it

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

```bash
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.

The image is the source of truth. A skill you delete from the image disappears from the
mirror on the next run, and anything written into `.claude/skills/` during a run is
replaced. The platform compares the mirror against what it wrote, so an edit made by the
agent does not survive, by design: skills are instructions the model follows, and a
self-modifying skill tree would be a way for injected text to persist across runs.

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.

## Claude Agent SDK

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`:

```python title="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](/guides/build/claude-agent-sdk) for the full authoring shape.

`skills` decides what the model is *shown*. An unlisted skill is hidden from the listing
and refused by the Skill tool, but its files are still in the workspace and still readable
by any tool that can read files. It is a context-management control, not a security
boundary. To keep something away from the agent, do not ship it in the image.

Leaving `skills` unset is not "skills off" either. Everything in the mirror is discovered.
Set it to a list when you want a narrower set.

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

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.

## Deep Agents

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

```python title="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,
    )
```

`SkillsMiddleware` registers no tools of its own. It lists each skill and tells the model
to `read_file` the path it advertises, and that read goes through **the graph's** backend.

So building the middleware yourself and passing it through `middleware=` compiles fine,
lists your skills, and then leaves the model unable to open any of them. The failure is
quiet: the skills appear in the listing and every attempt to load one comes back empty.
Passing `skills=` to `create_deep_agent` is what avoids it, because that builds the
middleware against the same `backend` the file tools use.

`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.

## Check that it loaded

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:

```json
{
  "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, tools, and connectors

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.

- **This page:** skills you ship *inside* your agent, for the agent's own model to use.
- [`tanomis skills install`](/getting-started/coding-agents): skills installed into *your*
  coding agent (Claude Code, Codex, Cursor) so it can drive the platform CLI. Nothing to do
  with your agent's runtime.
- [`delegation.skills`](/reference/agent-manifest): the units of work your agent *accepts*
  from other agents, declared in the manifest. A delegation contract, not a folder of
  instructions.

## Related

- [Session workspace and state](/guides/build/session-workspace-and-state): what else lives
  in the workspace and what survives a resume.
- [Claude Agent SDK](/guides/build/claude-agent-sdk) and [Deep Agents](/guides/build/deep-agents):
  the harness guides.
- [Agent manifest](/reference/agent-manifest): every field, including `delegation.skills`.
