Skip to content

Skills

Use Agent Skills to give an agent specialized instructions without putting every instruction in its initial prompt.

Point Skills at one or more skill libraries. The model first sees each skill’s name and description. When a skill is useful, the model loads it with the load_capability tool to receive that skill’s instructions.

Source

While Pydantic AI Harness is on 0.x releases, the API may change between minor releases; when it does, deprecation warnings and release-note migration guidance tell you (or your agent) exactly how to upgrade. See the version policy.

Installation

Install the skills extra for YAML frontmatter support:

Terminal
pip install "pydantic-ai-harness[skills]"

Quick start

Create a skill library:

.agents/skills/
  code-review/
    SKILL.md

Add the skill’s description and instructions:

---
name: code-review
description: Review a change for correctness and repository conventions.
---

Inspect the change and report findings by severity.

Then add the library to your agent, with a workspace to read it from:

from pydantic_ai import Agent
from pydantic_ai.capabilities import LocalWorkspace
from pydantic_ai_harness import Skills

agent = Agent(
    'anthropic:claude-opus-5-5',
    capabilities=[LocalWorkspace('.'), Skills('.agents/skills')],
)

Skills does not search .agents, .claude, or your home directory automatically. Pass each library you want it to load; to layer several the way coding agents do, see Layer project and personal skills.

How it works

At the start of every run, Skills:

  1. Reads each configured library from the run’s workspace. Relative paths resolve from its working directory.
  2. Scans the immediate child directories and validates the selected SKILL.md files.
  3. Offers each selected skill as a deferred capability named after it: the model sees its name and description, and loads it with the load_capability tool.

Loading a skill returns a # Skill: <name> heading, a line naming the skill’s directory in the workspace, and the skill’s Markdown body. The catalog is the same on every run over the same files, so it stays in the cached prefix; a new or renamed skill appears in it on the next run. Without any skills, Skills adds no instructions and no load_capability tool.

A run without a workspace fails at its start. To read skills from somewhere else, such as skills shipped with your application while the agent works in a sandbox, pass a workspace backend:

from pydantic_ai.workspaces import LocalWorkspaceBackend
from pydantic_ai_harness import Skills

skills = Skills('skills', workspace=LocalWorkspaceBackend('/app'))

workspace= takes a backend, not the LocalWorkspace capability. Under a durable engine such as Temporal, a workspace= backend is read in-process, not through the workflow.

Choose which skills to expose

By default, all discovered skills are included. Use include or exclude to change the catalog for a particular agent:

from pydantic_ai_harness import Skills

review_skills = Skills(
    '.agents/skills',
    include=['code-review'],
)

release_skills = Skills(
    '.agents/skills',
    exclude=['code-review'],
)
ConfigurationSkills in the catalog
Neither optionAll discovered skills
include=['a', 'b']Only a and b
include=[]No skills
exclude=['a', 'b']All except a and b
exclude=[]All discovered skills

include and exclude cannot be used together. The constructor overloads catch this in typed code, and runtime validation covers agent specs and untyped callers. Unknown names fail at run start.

Selection happens before frontmatter is parsed. An unselected skill does not add instructions or frontmatter validation errors to that Skills instance.

These options control catalog exposure. They are not filesystem permissions or an access-control boundary.

Directory paths choose where discovery starts in the workspace; they do not create a containment boundary, and normal symlink resolution applies.

A selected SKILL.md body becomes model instructions. Load libraries only from sources you trust, and review repository-provided skills before exposing them.

Skill format

Each immediate child directory containing SKILL.md is a skill:

.agents/skills/
  code-review/
    SKILL.md
  release-notes/
    SKILL.md

The loader uses these parts of SKILL.md:

PartRequirement
nameOptional. Defaults to the parent directory name. If provided, it must match the directory after Unicode normalization.
descriptionRequired and non-blank. The Agent Skills limit is 1,024 characters; longer descriptions load with a warning. This appears in the initial catalog.
Markdown bodyOptional. This is loaded under a generated # Skill: <name> heading.
disable-model-invocationOptional, true or false. true leaves the skill out of the model’s catalog; see Invoke a skill yourself.
user-invocableOptional, true or false. false tells a host not to offer the skill as a command; the model still sees it.

Skill names and include or exclude values are normalized with Unicode NFKC before matching. A normalized name can contain at most 64 lowercase Unicode letters or numbers, separated by single hyphens. It cannot start or end with a hyphen.

disable-model-invocation and user-invocable come from Claude Code. Any value other than true or false (in any letter case) skips the skill with a warning, rather than guessing whether it may be invoked.

Only immediate children are discovered. For example, code-review/references/SKILL.md does not create another skill. Ordinary files and child directories without SKILL.md are ignored.

You can pass several libraries:

from pydantic_ai_harness import Skills

skills = Skills([
    '.agents/skills',
    'company/skills',
])

By default, selected skill names must be unique across those libraries. Repeated references to the same resolved library are scanned once, and a skill found twice, through a symlinked library or skill directory or as a byte-identical copy of its SKILL.md, counts once.

Layer project and personal skills

Coding agents read skills from conventional locations that a project may not have, and let a project’s skill take precedence over a personal one with the same name. Two options give Skills the same behavior:

from pydantic_ai_harness import Skills

skills = Skills(
    ['.agents/skills', '.claude/skills', '/home/me/.agents/skills', '/home/me/.claude/skills'],
    missing_directories='skip',
    duplicate_names='keep_first',
)
  • missing_directories='skip' leaves out a library directory that does not exist, instead of failing the run. A path that is a file, or that holds a SKILL.md itself (a skill package rather than a library of them), is left out with a UserWarning, so one a model creates mid-session doesn’t fail every later run.
  • duplicate_names='keep_first' keeps the skill from the earlier directory when two valid skills with different SKILL.md files share a name, and skips the other with a UserWarning. An invalid SKILL.md is skipped first, so it does not hide a valid skill with its name.

Directories are listed in precedence order. Paths are workspace paths, and ~ is not expanded, because the workspace may be a sandbox with a home directory of its own; spell out the absolute path for a library on this machine.

If .agents/skills is a symlink to .claude/skills, or holds byte-identical copies of its SKILL.md files, as in many repositories, each skill is listed once. Only SKILL.md is compared: the first directory’s bundled files are the ones the model is pointed at.

Bundled files

Agent Skill packages can contain directories such as references/, assets/, and scripts/. Skills does not enumerate, read, or execute those files.

The loaded instructions name the skill’s directory in the run’s workspace, so a model with file or shell tools, such as those from FileSystem or Shell, can follow a relative reference like references/guide.md or run scripts/check.py. Skills read from a workspace= backend are not where those tools work, so their instructions leave the directory out. Placeholders such as ${CLAUDE_SKILL_DIR} remain unchanged in the body.

Skills reads the libraries itself: the model does not need FileSystem or Shell to load a skill, and adding either does not change which files Skills reads.

Compatibility with existing skill libraries

The portable name, description, and Markdown instructions are supported. name may be omitted and derived from the directory.

The following behavioral fields are accepted for compatibility, but their behavior is not implemented:

agent, allowed-tools, argument-hint, arguments, context, dependencies,
disallowed-tools, effort, hooks, model, paths, shell, tools, when_to_use

If a selected skill uses any of these fields, the run emits one aggregated UserWarning at its start. Fields such as license, compatibility, and metadata are accepted without changing runtime behavior. Other unknown, non-behavioral fields are also accepted.

Invoke a skill yourself

The model decides when to load a skill. A host that also lets a person invoke a skill, such as a /code-review src/app.py command in a terminal client, reads the same catalog with load and renders the skill as a prompt:

from pydantic_ai.workspaces import LocalWorkspaceBackend
from pydantic_ai_harness import Skills

skills = Skills('.agents/skills', missing_directories='skip')
catalog = await skills.load(LocalWorkspaceBackend('.'))
review = next(skill for skill in catalog.skills if skill.name == 'code-review')
result = await agent.run(review.render('src/app.py'))

load reads the libraries as a run would, without emitting warnings. Each entry in the catalog’s skipped is a SkippedSkill with the path left out, a reason, and the message a run would warn with, for the host to show:

reasonLeft out
'invalid'A SKILL.md with malformed frontmatter, invalid UTF-8, or an invalid or mismatched name
'duplicate_name'A SKILL.md named like a skill found earlier, with duplicate_names='keep_first'
'not_a_directory'A library path that is a file, with missing_directories='skip'
'skill_package'A library path that holds a SKILL.md itself, with missing_directories='skip'

Each SkillDefinition has the skill’s name, description, body, path, directory, in_run_workspace, model_invocable, and user_invocable. The catalog includes skills with disable-model-invocation: true (model_invocable is False), which a run leaves out of the model’s catalog, so a host can offer them as commands only. Skills doesn’t offer commands itself, so user_invocable is for the host to check before it offers one.

render(arguments) returns what loading the skill returns, with every $ARGUMENTS in the body replaced by arguments. A body without $ARGUMENTS gets ARGUMENTS: <arguments> appended. Other placeholders, such as Claude Code’s indexed $0 or $ARGUMENTS[0], are left unchanged, and do not count as $ARGUMENTS. render() without arguments returns the instructions the model gets when it loads the skill, including leaving the directory out for a skill read from workspace= (in_run_workspace is False).

Use an agent spec

Skills works with Pydantic AI’s YAML and JSON agent specs:

model: anthropic:claude-opus-5-5
capabilities:
  - Skills:
      directories: .agents/skills
      include:
        - code-review
        - release-notes

Register Skills when loading the spec:

from pydantic_ai import Agent
from pydantic_ai_harness import Skills

agent = Agent.from_file('agent.yaml', custom_capability_types=[Skills])

The skills extra installs PyYAML. It parses SKILL.md frontmatter and YAML agent specs.

Define capabilities in Python

Use Pydantic AI’s core Capability when your instructions or tools are defined in Python instead of a SKILL.md package:

from pydantic_ai.capabilities import Capability

refunds = Capability(
    id='refunds',
    description='Use for refund policy questions.',
    instructions='Check the refund policy before answering.',
    defer_loading=True,
)

Skills loads portable Agent Skill packages. It does not replace the core API for code-defined capabilities.

Configuration

Skills(
    directories: str | Path | Sequence[str | Path],
    *,
    include: Collection[str] | None = None,
    exclude: Collection[str] | None = None,
    missing_directories: Literal['error', 'skip'] = 'error',
    duplicate_names: Literal['error', 'keep_first'] = 'error',
    workspace: WorkspaceBackend | None = None,
)
  • directories accepts one library path or a sequence of paths.
  • include exposes only the named skills.
  • exclude omits the named skills from the catalog.
  • missing_directories='skip' leaves out library paths that do not exist, and warns about and leaves out those that are files or skill packages.
  • duplicate_names='keep_first' keeps the first of two skills with one name, with a warning.
  • workspace reads the libraries from this backend instead of the run’s workspace.

Pass at least one library directory, not the path of an individual skill package. Malformed frontmatter, invalid UTF-8, and invalid or mismatched names warn and skip that skill. Unknown selections fail at run start, and so do duplicate selected names, missing libraries, and library paths that are files or skill packages, unless duplicate_names or missing_directories say otherwise.

Two Skills on one agent combine into one catalog.

Further reading

API reference

Skills

Bases: AbstractCapability[AgentDepsT]

Offer Agent Skill instructions from the run’s workspace as deferred capabilities.

Skill libraries are directories in the run’s workspace (ctx.workspace), read at the start of every run; relative paths resolve against its working directory. Attach LocalWorkspace to read directories on this machine, or pass workspace= to read them from a workspace of their own. A run with neither fails at its start.

Each selected immediate child containing SKILL.md becomes a deferred capability named after the skill: the model sees its name and description, and loads its Markdown body, with the skill’s directory in the run’s workspace, with load_capability. Bundled files are not loaded or executed. Descriptions longer than the Agent Skills limit are preserved and emit a warning. A skill found twice, through a symlink or as a byte-identical SKILL.md, counts once. A skill whose frontmatter has disable-model-invocation: true is left out of the model’s catalog.

load reads the same skills outside a run, for a host that lets a person invoke a skill itself; render one with SkillDefinition.render.

Two Skills on one agent combine, so every library either names stays reachable.

Attributes

id

One per agent: two Skills combine into one catalog.

Type: str | None Default: 'skills'

directories

Skill-library paths in the workspace, read at the start of each run.

Type: tuple[str | Path, …] Default: self._normalize_directories(directories)

include

Exact skill names to expose, or None to expose all discovered skills.

Type: frozenset[str] | None Default: self._normalize_selection('include', include) if include is not None else None

exclude

Exact skill names to omit from the catalog.

Type: frozenset[str] Default: self._normalize_selection('exclude', exclude) if exclude is not None else frozenset()

missing_directories

'error' fails the run on a library path that is not a library; 'skip' leaves it out, with a warning unless it does not exist.

Type: MissingDirectories Default: missing_directories

duplicate_names

'error' fails the run when two different SKILL.md files share a name; 'keep_first' keeps the first.

Type: DuplicateNames Default: duplicate_names

workspace

Where the libraries live, when not in the run’s workspace; see __init__.

Type: WorkspaceBackend | None Default: workspace

Methods

__init__
def __init__(
    directories: str | Path | Sequence[str | Path],
    *,
    include: Collection[str],
    exclude: None = None,
    missing_directories: MissingDirectories = 'error',
    duplicate_names: DuplicateNames = 'error',
    workspace: WorkspaceBackend | None = None,
) -> None
def __init__(
    directories: str | Path | Sequence[str | Path],
    *,
    include: None = None,
    exclude: Collection[str] | None = None,
    missing_directories: MissingDirectories = 'error',
    duplicate_names: DuplicateNames = 'error',
    workspace: WorkspaceBackend | None = None,
) -> None

Configure the skill libraries to read at the start of each run.

Returns

None

Parameters

directories : str | Path | Sequence[str | Path]

One skill-library path or a sequence of paths in the workspace, in precedence order. ~ is not expanded: the workspace may be a sandbox with a home of its own.

include : Collection[str] | None Default: None

Exact names to expose. Omit to expose all discovered skills.

exclude : Collection[str] | None Default: None

Exact names to omit. Cannot be combined with include.

missing_directories : MissingDirectories Default: 'error'

'error' (the default) fails the run when a library path does not exist, is not a directory, or is itself a skill package. 'skip' leaves such a path out, for conventional locations such as .agents/skills that a project may not have: one that does not exist quietly, any other with a warning.

duplicate_names : DuplicateNames Default: 'error'

'error' (the default) fails the run when two different SKILL.md files share a name. 'keep_first' keeps the one from the earlier directory and skips the other with a warning, the way coding agents layer project skills over personal ones.

workspace : WorkspaceBackend | None Default: None

A workspace backend to read the libraries from instead of the run’s, such as LocalWorkspaceBackend('/app') for skills shipped with the code while the agent works in a sandbox. It is read in-process only in this release: a durable engine does not route it through its workflow machinery.

__repr__
def __repr__() -> str

Show only the Skills configuration that callers control.

Returns

str

combine

@classmethod

def combine(
    cls,
    capabilities: Sequence[AbstractCapability[AgentDepsT]],
) -> AbstractCapability[AgentDepsT]

Serve every combined configuration’s libraries through one catalog.

The field-by-field default would keep only the last configuration’s directories, dropping the other libraries. A skill name selected by two configurations must name the same SKILL.md, or a byte-identical copy, unless the later configuration has duplicate_names='keep_first'.

Returns

AbstractCapability[AgentDepsT]

for_run

@async

def for_run(ctx: RunContext[AgentDepsT]) -> AbstractCapability[AgentDepsT]

Read the selected skills, from each configuration’s workspace= or else the run’s workspace.

Returns one deferred capability per skill the model may load, or, without any, this capability, which adds no instructions or tools. Emits a UserWarning for each skipped SKILL.md or library path, an overlong description, or unsupported frontmatter. Raises UserError when a configuration without workspace= meets a run without a workspace.

Returns

AbstractCapability[AgentDepsT]

load

@async

def load(workspace: WorkspaceBackend | None = None) -> SkillCatalog

Read the selected skills now, as a run does at its start, without emitting warnings.

For a host that offers skills to a person as well as to the model, such as a /code-review command that sends skill.render(arguments) as a prompt. The catalog includes skills the model may not load (model_invocable), and its skipped entries say which SKILL.md files and library paths were left out, for the host to show.

Returns

SkillCatalog

Parameters

workspace : WorkspaceBackend | None Default: None

Where libraries without their own workspace= live, as the run’s workspace would be, such as LocalWorkspaceBackend('.').

Raises
  • UserError — A configuration without workspace= needs workspace, and none was passed.
  • ValueError — A configuration problem that would also fail a run, such as an unknown include name or, with duplicate_names='error', two skills with one name.

SkillDefinition

Validated skill metadata and body from a single SKILL.md.

Attributes

name

The skill’s name: its name frontmatter field, or else its directory’s name.

Type: str

description

The description frontmatter field, listed in the model’s catalog.

Type: str

body

The Markdown after the frontmatter, without leading and trailing blank lines.

Type: str

ignored_behavioral_fields

Frontmatter fields such as allowed-tools whose behavior Skills does not implement.

Type: tuple[str, …]

model_invocable

Whether the model may load it; False when its frontmatter has disable-model-invocation: true.

A run leaves such a skill out of the model’s catalog. Skills.load still returns it, for a host that lets a person invoke it.

Type: bool Default: True

user_invocable

Whether a host should let a person invoke it; False when its frontmatter has user-invocable: false.

Skills itself only offers skills to the model, so this is for a host, such as one with a /skill-name command.

Type: bool Default: True

path

Absolute workspace path of the SKILL.md it was read from.

Type: str

in_run_workspace

Whether it was read from the run’s workspace, where the model’s file and shell tools can reach its directory.

False for a library read from Skills(workspace=...); render then leaves the directory out.

Type: bool Default: True

directory

Absolute workspace path of the skill’s directory, which holds any scripts/ or references/.

Type: str

Methods

render
def render(arguments: str | None = None) -> str

The skill’s instructions as the model receives them: a heading, the skill’s directory, and the body.

Pass arguments when a user invokes the skill with a command such as /code-review src/app.py: every $ARGUMENTS in the body becomes arguments, and a body without $ARGUMENTS gets ARGUMENTS: <arguments> appended. Other placeholders, such as Claude Code’s indexed $0 and $ARGUMENTS[0], are left unchanged. Without arguments, the result is the instructions the model gets when it loads the skill.

The directory line is left out unless in_run_workspace.

Returns

str

SkillCatalog

The skills Skills.load read, and each SKILL.md or library it left out.

Run-time advice, such as frontmatter fields whose behavior is not implemented, is not included: a run emits it as a UserWarning, and each skill’s ignored_behavioral_fields lists its own.

Attributes

skills

The selected, valid skills, in catalog order, including those the model may not load.

Type: tuple[SkillDefinition, …]

skipped

Each SKILL.md or library path left out, in the order it was found.

A run emits each message as a UserWarning; a host showing skills to a person can show them instead.

Type: tuple[SkippedSkill, …]

SkippedSkill

A SKILL.md or skill library path that Skills left out, and why.

Attributes

path

Absolute workspace path of the skipped SKILL.md, or of the skipped library.

Type: str

reason

Why it was left out.

Type: SkipReason

message

A sentence naming the path and the problem, as a run’s UserWarning says it.

Type: str