Filesystem-based agent skill loader: spec + reference implementation.
Project description
outskilled
Filesystem-based agent skill loader. One spec, one reference
implementation, with a first-party adapter for pydantic-ai.
A skill is a folder containing a SKILL.md file — YAML
frontmatter describing what the skill does plus a markdown body
explaining how to do it. outskilled discovers skills (arbitrary
nesting supported), validates them against the spec, and
renders a system-prompt-ready manifest. The pydantic-ai adapter wires
the manifest into an Agent's instructions and registers a
load_skill tool the model uses to fetch a skill's body on demand
(plus opt-in tools for listing skills and reading skill resources).
Two lines of glue and the model can route to the right skill.
References
Spec & ecosystem:
- https://agentskills.io/home
- https://agentskills.io/skill-creation/best-practices
- https://github.com/agentskills/agentskills
- https://code.claude.com/docs/en/skills
- https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills
- https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf
Pydantic-AI:
- https://pydantic.dev/docs/ai/overview/coding-agent-skills/
- https://github.com/pydantic/skills
- https://pydantic.dev/docs/ai/core-concepts/capabilities/
Status
v0.2 — spec stabilising. Adds optional when_to_use and
always_load frontmatter fields and a pydantic-ai adapter alongside
the framework-agnostic registry. v0.1 skills continue to validate
unchanged.
Quickstart
from pathlib import Path
from outskilled import SkillRegistry
# Either pass roots directly...
reg = SkillRegistry([Path("./skills")])
# ...or load them from a config file that lives with your skills:
reg = SkillRegistry.from_config("./skills/skills.yaml")
# Level 1: render the manifest for the system prompt.
print(reg.manifest_xml())
# Level 2: load a skill's body on demand. (The `search` skill ships
# with the example bundle under examples/agent_demo/skills/.)
body = reg.load("search")
SkillRegistry walks every directory under its roots, treats any
directory containing SKILL.md as a skill, and validates against the
spec. Reserved subdirectories inside a skill (references/,
scripts/, assets/) are not descended into. Arbitrary nesting is
supported — the path from a skill root to a skill's parent directory
is the category path. The same loader handles flat layouts and
N-level categories without a nested=True flag.
skills.yaml
A small declarative config (lives inside your skills directory, so the bundle is portable):
# skills/skills.yaml
roots:
- . # paths are resolved relative to this file
# - ../shared-skills # optional extra roots
SkillRegistry.from_config(path) resolves the listed paths relative
to the config file's directory.
Pydantic-AI integration
Install the extra:
pip install "outskilled[pydantic-ai]"
Attach a registry to an existing Agent:
from pydantic_ai import Agent
from outskilled.pydanticai import attach_skills
agent = Agent("anthropic:claude-sonnet-4-6")
attach_skills(agent, "skills/skills.yaml")
Or build an Agent with skills already wired up:
from outskilled.pydanticai import skill_aware_agent
agent = skill_aware_agent(
"anthropic:claude-sonnet-4-6",
skills="skills/skills.yaml",
)
What attach_skills adds (each independently togglable):
- Instructions (Level 1, on by default): an
@agent.instructionsfunction that returns the rendered<available_skills>manifest, sorted for deterministic prompt caching. load_skill(name)tool (Level 2, on by default): the model calls this to fetch a skill body on demand. Path-traversal-safe.- Inlined always-loaded bodies (on by default): any skill with
always_load: truein its frontmatter has its body inlined into the instructions. Independent of the manifest flag — bodies are inlined even when the manifest is suppressed. list_skillstool (opt-in): re-renders the manifest at runtime.read_skill_resourcetool (Level 3, opt-in): exposes files under a skill'sreferences/,scripts/,assets/with path safety.
attach_skills is not idempotent — calling it twice on the same
Agent raises SkillError. Build a new Agent if you need a
different skill set.
Try it
A self-contained demo lives under examples/agent_demo/ — four
skills across three nesting depths, an offline plumbing check, and a
real routing eval against Claude Sonnet 4.6.
pip install -e ".[pydantic-ai]"
# Plumbing check (no API key). Verifies the manifest + load_skill
# round-trip, NOT whether an LLM routes correctly.
python examples/agent_demo/run_demo.py
# Real routing eval against an actual model.
export ANTHROPIC_API_KEY=...
python examples/agent_demo/run_live.py
See examples/agent_demo/README.md for the two-script split and what
each one does / doesn't prove.
Install
pip install -e .
Python 3.12+. One runtime dep: PyYAML.
Spec
See SPEC.md for the canonical rules: directory layout,
frontmatter schema, validation, manifest format, progressive
disclosure.
Layout
outskilled/
├── SPEC.md
├── README.md
├── LICENSE
├── pyproject.toml
├── src/outskilled/
│ ├── __init__.py # public API
│ ├── errors.py # typed exceptions
│ ├── models.py # Skill dataclass
│ ├── parser.py # frontmatter extraction
│ ├── validator.py # spec §3 rules
│ ├── manifest.py # XML + markdown renderers
│ ├── registry.py # discovery + composition
│ └── pydanticai.py # optional pydantic-ai adapter
├── examples/
│ └── agent_demo/ # end-to-end demo + offline & live runners
├── tests/
└── .github/workflows/
└── publish.yml # PyPI publish on Release
Development
pip install -e ".[dev]"
pytest
Release
Publishing is automated via .github/workflows/publish.yml. To cut a
release:
- Bump
versioninpyproject.tomland__version__insrc/outskilled/__init__.py(keep them in sync — the workflow checks the tag againstpyproject.toml). - Commit, merge to
main. - On GitHub, Releases → Draft a new release, create a tag
matching the bumped version (e.g.
v0.2.1), publish.
The workflow runs the test suite, builds sdist + wheel, and uploads to PyPI via trusted publishing (OIDC — no API token in secrets).
One-time PyPI setup (only needed on the first release): on PyPI, Your projects → outskilled → Publishing → Add a new publisher:
| Field | Value |
|---|---|
| Owner | phiweger |
| Repository | outskilled |
| Workflow filename | publish.yml |
| Environment name | pypi |
For the very first release (before the project exists on PyPI), use the pending publisher flow under Your account → Publishing → Add a pending publisher.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file outskilled-0.1.0.tar.gz.
File metadata
- Download URL: outskilled-0.1.0.tar.gz
- Upload date:
- Size: 35.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12869b10e436f4554d978fa6b63966255316abb5c0a1ea8aeaa298d2d6dc8635
|
|
| MD5 |
5c1f5ff1dec74f4408e81cb44f0a0da7
|
|
| BLAKE2b-256 |
9bba0eb06056c69cf5b56adc3ffb5f29e4711dc64be6e86d276806edd4d30fbb
|
Provenance
The following attestation bundles were made for outskilled-0.1.0.tar.gz:
Publisher:
publish.yml on phiweger/outskilled
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
outskilled-0.1.0.tar.gz -
Subject digest:
12869b10e436f4554d978fa6b63966255316abb5c0a1ea8aeaa298d2d6dc8635 - Sigstore transparency entry: 1587161367
- Sigstore integration time:
-
Permalink:
phiweger/outskilled@28fd3f740b4a5ab972b9accd9e0a3ebccaac2ff2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/phiweger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@28fd3f740b4a5ab972b9accd9e0a3ebccaac2ff2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file outskilled-0.1.0-py3-none-any.whl.
File metadata
- Download URL: outskilled-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a11f4a15840409479ec9a6113b4ff016f574accb8a503b266f6d63794353e658
|
|
| MD5 |
b5c852b4da4be7b9153081ba0ca76197
|
|
| BLAKE2b-256 |
3a4f4e07df4ce33886971c50986061759301a2cb5b449e43b8e8a8c49229cc11
|
Provenance
The following attestation bundles were made for outskilled-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on phiweger/outskilled
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
outskilled-0.1.0-py3-none-any.whl -
Subject digest:
a11f4a15840409479ec9a6113b4ff016f574accb8a503b266f6d63794353e658 - Sigstore transparency entry: 1587161834
- Sigstore integration time:
-
Permalink:
phiweger/outskilled@28fd3f740b4a5ab972b9accd9e0a3ebccaac2ff2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/phiweger
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@28fd3f740b4a5ab972b9accd9e0a3ebccaac2ff2 -
Trigger Event:
release
-
Statement type: