> ## Documentation Index
> Fetch the complete documentation index at: https://docs.decimal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Native Skill Loaders

> The eight agent frameworks that read installed skill folders themselves: the one line that points each at your skills, what DecimalAI records when one loads a skill, and the gaps each has upstream.

Eight agent frameworks load Agent Skills from disk on their own: they read `SKILL.md` folders, offer the skills to the model, and give it tools to load a skill, read its files and run its scripts. [`decimalai skills add`](/guides/skills-on-disk) writes folders all eight accept. This page is the one line that points each framework at them, and what DecimalAI records when the framework loads one.

Each framework has a helper in its `decimalai.<framework>` module (`pip install "decimalai>=0.16"`). It points the framework's own loader at your installed skills and, with DecimalAI's instrumentation on, every skill the loader hands the model is recorded as an **activation of the exact version the model received**. DecimalAI's skill router stays out of an agent whose own loader serves its skills, so the model sees one catalog.

## What DecimalAI records

When the framework's loader delivers a skill to the model, the trace for that run gets an activation carrying the skill's registry id and **version id**. The version is decided from the bytes the model received, never from the folder on disk:

1. the identity stamp in the delivered copy (`metadata.decimalai-*`), when the delivered body's hash matches it;
2. otherwise the lock, by the delivered body's hash;
3. otherwise any version this process has already seen with that hash.

A body that is none of these (a skill you edited by hand, for example) is reported with its skill id and hash and no version, and the platform resolves the version if it can.

This matters after [`decimalai skills update`](/guides/skills-on-disk). Some loaders keep serving a skill they already loaded, so the folder holds version 4 while the model still reads version 3, and the activation then says version 3:

| After `skills update`, the same agent... | Frameworks |
| - | - |
| keeps serving the version it loaded | Google ADK, Microsoft Agent Framework, Strands |
| reads the new version | Claude Agent SDK, CrewAI, OpenAI Agents, Deep Agents, Pydantic AI harness |

Also on the trace:

* **A skill loaded twice in one run is one activation.**
* **The skills the loader listed** for the model are recorded as offered.
* **The router steps aside.** An agent whose own loader serves its skills gets no DecimalAI skill list and no DecimalAI `load_skill` tool; the SDK logs this once at INFO, or as a warning on an OpenAI Agents, Pydantic AI or Google ADK agent you also gave `agent_config()`'s skills. If both still reach the model, because the loader was attached where the adapter could not see it, a WARNING says so once.

## Setup at a glance

| Framework | Turn on DecimalAI | Point the loader at your skills |
| - | - | - |
| Claude Agent SDK | `decimalai.init(claude_agent_sdk=True)` | `ClaudeAgentOptions(plugins=[decimalai.claude_agent_sdk.skills_plugin()])` |
| Google ADK | `App(..., plugins=[decimalai.adk.DecimalAIPlugin()])` | `SkillToolset(skills=decimalai.adk.skills(), ...)` |
| CrewAI | `decimalai.init(crewai=True)` | `Agent(..., skills=decimalai.crewai.skills(), tools=decimalai.crewai.skill_tools())` |
| OpenAI Agents (SandboxAgent) | `decimalai.init(openai_agents=True)` | `capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()]` |
| Deep Agents | `decimalai.init(langchain=True)` | `create_deep_agent(model, **decimalai.deepagents.skills())` |
| Microsoft Agent Framework | `decimalai.init(otel=True, agent_name=...)` | `Agent(..., context_providers=[decimalai.agent_framework.skills()])` |
| Pydantic AI harness | `decimalai.init(otel=True, agent_name=...)`, `Agent.instrument_all()` | `capabilities=[LocalWorkspace(project), decimalai.pydantic_ai_harness.skills(project), ...]` |
| Strands Agents | `decimalai.init(otel=True, agent_name=...)` | `Agent(..., plugins=[decimalai.strands.skills()])` |

Each helper lives in its framework's module; import the module before you use it (`import decimalai.adk`): `import decimalai` alone does not load it. Microsoft Agent Framework, Strands and the Pydantic AI harness need no DecimalAI adapter: with `init(otel=True)` (and, on Pydantic AI, `Agent.instrument_all()`) their own OpenTelemetry spans become the trace, and the load is recorded on it. Pass `agent_name` there, since the exporter names the trace by it. The Pydantic AI harness needed `decimalai.pydantic_ai.instrument()` as well through SDK 0.18; 0.19 removed it. Run inside `decimalai.start_trace()` and the load is recorded on that trace instead.

## `decimalai.skills_dir()`

The helpers start from `decimalai.skills_dir()`; you can use it directly too.

```python theme={null}
import decimalai
from pathlib import Path

decimalai.skills_dir()                         # /home/me/support-bot/.agents/skills
decimalai.skills_dir(relative_to=Path.cwd())   # .agents/skills
```

* It finds the project from the working directory, as every `decimalai skills` command does, so it works from any subdirectory of the project. `skills_dir(directory=...)` starts the search somewhere else.
* It returns the absolute, symlink-resolved path. With `relative_to=`, it returns the path relative to that directory instead, for the two loaders that need one (OpenAI Agents and the Pydantic AI harness), and raises `UsageError` when the root is not under it.
* It raises `SkillsNotInstalledError` when the project has no lock or no root yet (run `decimalai skills install`), `LegacyLockError` for a lock from SDK 0.14 or older (run `decimalai skills migrate`), and `IllegalEntryError` when `.agents` or `.agents/skills` is a symlink.

## The eight loaders

| Loader | Entry point | The model loads a skill with | Reads a bundled file with | Runs a script with |
| - | - | - | - | - |
| Claude Agent SDK and Claude Code (`claude-agent-sdk`) | the `.agents` folder as a plugin | the Skill tool | `Read` | `Bash` |
| Google ADK (`google-adk`) | `SkillToolset` | `list_skills`, `load_skill` | `load_skill_resource` | `run_skill_script` (needs a code executor) |
| CrewAI (`crewai`) | `Agent(skills=...)` | `load_skill` | `load_skill_resource` (from `skill_tools()`) | `run_skill_script` (from `skill_tools()`) |
| OpenAI Agents SandboxAgent (`openai-agents`) | the sandbox `Skills` capability | `load_skill` | `exec_command` (the `Shell` capability) | `exec_command` |
| Deep Agents (`deepagents`) | `create_deep_agent(skills=...)` | the skill list in its system prompt, then `read_file` | `read_file` | `execute` (needs a shell backend) |
| Microsoft Agent Framework (`agent-framework`) | `SkillsProvider` | `load_skill` | `read_skill_resource` | `run_skill_script` (the helper's runner) |
| Pydantic AI harness (`pydantic-ai-harness`) | the harness `Skills` capability | `load_capability` | `read_file` (`FileSystem`) | `run_command` (`Shell`) |
| Strands Agents (`strands-agents`) | the `AgentSkills` plugin | the `skills` tool | `file_editor` | `shell` |

Each snippet below is the documented setup that the SDK's own test suite runs against the real framework. In the suite a scripted model stands in for yours; `model`, `llm` and `client` in the snippets are your framework's own model objects.

## Claude Agent SDK and Claude Code

The Claude Agent SDK drives the Claude Code CLI, and both load `.agents` as a [plugin](/guides/skills-on-disk#what-skills-add-writes) named `decimalai-skills`: its `skills/` folder is the root the other seven read.

```python theme={null}
import asyncio

import decimalai
from claude_agent_sdk import ClaudeAgentOptions, query
from decimalai.claude_agent_sdk import skills_plugin

decimalai.init(claude_agent_sdk=True)

options = ClaudeAgentOptions(
    plugins=[skills_plugin()],   # the project's .agents, as an absolute path
    skills="all",                # offer every skill it finds, through the Skill tool
)


async def main():
    async for message in query(prompt="A customer wants a refund for $100 after 45 days.",
                               options=options):
        print(message)


asyncio.run(main())
```

Interactively:

```bash theme={null}
claude --plugin-dir "$(git rev-parse --show-toplevel)/.agents"
```

* The plugin path must be **absolute**: a relative `--plugin-dir .agents` started from a subdirectory loads nothing, with exit status 0 and nothing on stderr. `skills_plugin()` returns an absolute one.
* Skills are listed as `decimalai-skills:<install-name>`. The plugin is not named `decimalai`, the [marketplace plugin](/guides/claude-code)'s name, so the two load side by side.
* It reads `name`, `description` and the body, and acts on `allowed-tools` and `disable-model-invocation`. It pre-approves the tools a skill lists: a skill your organization authored can run its own bundled scripts with no prompt (`Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/… *)`), any other command needs permission, and a skill your organization did not author lists only `Read`, `Grep` and `Glob`, so its scripts always need permission. A user-only skill (`disable-model-invocation: true`) is listed but never offered to the model.
* The frontmatter never reaches your process, so the version comes from the lock, by the hash of the body the CLI delivered.

Graded at claude-agent-sdk 0.2.166 (Claude Code 2.1.299).

## Google ADK

```python theme={null}
import decimalai
import decimalai.adk
from google.adk.agents import LlmAgent
from google.adk.apps import App
from google.adk.code_executors.unsafe_local_code_executor import UnsafeLocalCodeExecutor
from google.adk.tools.skill_toolset import SkillToolset

decimalai.init(agent_name="support")

agent = LlmAgent(
    name="support",
    model="gemini-3.5-flash",
    instruction="Answer refund questions.",
    tools=[SkillToolset(skills=decimalai.adk.skills(), code_executor=UnsafeLocalCodeExecutor())],
)
app = App(name="support", root_agent=agent, plugins=[decimalai.adk.DecimalAIPlugin()])
```

* The plugin in the `App` traces each invocation and records each load (SDK 0.20+; through 0.19, `decimalai.init(adk=True)` added it to every runner).

* `decimalai.adk.skills()` loads each folder on its own and skips a bad one with a WARNING that names it. ADK's own `load_skills_from_dir` raises on one bad folder and loads nothing at all.

* It reads each folder's frontmatter, `metadata` included, and the body. The model first lists the skills, then loads one.

* ADK parses the skills when `skills()` runs, so an agent built before `decimalai skills update` keeps serving the old version until you build it again. The activation says which version that was.

* Without a `code_executor`, ADK offers no `run_skill_script`. `UnsafeLocalCodeExecutor` runs scripts on your machine with no sandbox; use it only for skills you trust.

Graded at google-adk 2.12.0.

## CrewAI

```python theme={null}
import decimalai
import decimalai.crewai
from crewai import Agent, Crew, Task

decimalai.init(crewai=True)

agent = Agent(role="Support", goal="Answer refund questions", backstory="A support agent.",
              llm=llm, skills=decimalai.crewai.skills(), tools=decimalai.crewai.skill_tools())
task = Task(description="Refund for $100 after 45 days?", expected_output="an answer",
            agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()
```

* CrewAI's own skill tool is `load_skill`, which delivers the body. CrewAI has **no resource reader and no script runner** of its own, so `skill_tools()` adds two: `load_skill_resource` reads a skill's `references/`, `assets/` or `scripts/` file, and `run_skill_script` runs one of its `scripts/` (Python with your interpreter, shell scripts with `bash`, no shell expansion, with a timeout). Both accept only installed skills and paths inside them. `run_skill_script` does not enforce a skill's `allowed-tools`; `skill_tools(scripts=False)` leaves it out.
* It has the strictest `metadata` parser of the eight: every value must be a string. A bad folder is skipped with a `SkillLoadFailedEvent` and a log line, not an exception.
* CrewAI reads a skill's body from disk on every load, so it serves the new version right after `decimalai skills update`.

Graded at crewai 1.15.27.

## OpenAI Agents (SandboxAgent)

```python theme={null}
import asyncio

import decimalai
from agents import RunConfig, Runner
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
from decimalai.openai_agents import sandbox_skills

decimalai.init(openai_agents=True)

agent = SandboxAgent(
    name="support",
    instructions="Answer refund questions.",
    capabilities=[Shell(), sandbox_skills()],   # run from the project root
    default_manifest=Manifest(),
)
config = RunConfig(sandbox=SandboxRunConfig(client=UnixLocalSandboxClient()))
asyncio.run(Runner.run(agent, "Refund for $100 after 45 days?", run_config=config))
```

* `sandbox_skills()` is the sandbox's `Skills(lazy_from=LocalDirLazySkillSource(...))` over `skills_dir(relative_to=Path.cwd())`. Run from the project root: a source outside the working directory, reached through a symlink or through the macOS `/tmp` alias, lists **zero** skills.
* It adds two guards the plain capability lacks. The sandbox skips a symlinked skill folder without a word, so `sandbox_skills()` warns when it would list nothing. And the sandbox's `load_skill` aborts the whole run on a name it cannot find; the helper's answers the model with the names it can load instead.
* **Keep `default_manifest=Manifest()`**; without it the skills vanish.
* When the model loads a skill, the sandbox copies its folder to `.agents/<install-name>` and drops the exec bit on the way, so run scripts through their interpreter (`python3 scripts/compute_refund.py`), never as `./scripts/...`. DecimalAI identifies the load from that copy, the bytes the model reads.
* On macOS, commands run under `sandbox-exec`.

Graded at openai-agents 0.23.1.

## Deep Agents

```python theme={null}
import decimalai
import decimalai.deepagents
from deepagents import create_deep_agent

decimalai.init(langchain=True)

agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Refund for $100 after 45 days?"}]})
```

* `decimalai.deepagents.skills()` returns the two arguments Deep Agents needs together: `skills=[<your skills folder>]` and a `backend` that can see it. The default backend is a `FilesystemBackend(root_dir=<project>, virtual_mode=False)`, which reads files but has **no shell**, so a skill's scripts cannot run. To run them, pass one: `decimalai.deepagents.skills(backend=LocalShellBackend(root_dir=project, virtual_mode=False))`. `LocalShellBackend` runs commands on your machine.
* **`virtual_mode=False`.** With `True`, the prompt advertises virtual `/skills/...` paths that `execute` cannot open.
* The skills middleware lists each skill in the system prompt with the path of its `SKILL.md`; the model reads it with `read_file`. A read that stops short of the whole file delivers part of the body, and the activation then carries no version, because that body is no version.
* A namespaced or uppercase `name:` is listed under that name, with a warning on the `deepagents` logger, rather than under its folder's.

Graded at deepagents 0.7.25.

## Microsoft Agent Framework

```python theme={null}
import asyncio

import decimalai
from agent_framework import Agent
from agent_framework.observability import enable_sensitive_telemetry
from decimalai.agent_framework import skills

decimalai.init(otel=True, agent_name="support")
enable_sensitive_telemetry()   # tool arguments and results on the trace

agent = Agent(client=client, instructions="Answer refund questions.",
              context_providers=[skills()])
asyncio.run(agent.run("Refund for $100 after 45 days?"))
```

* `skills()` is `SkillsProvider.from_paths(...)` over your skills folder, with three things added on the same object: the observer, a **script runner** (MAF ships none; without one a script answers that it requires a runner), and an **approval rule**. MAF makes every skill tool wait for approval by default; the helper lets skill loads and file reads run unasked, and keeps scripts waiting for approval unless you pass `skills(auto_approve_scripts=True)`.
* **It caches skills by default**, so a long-running provider keeps serving the old version after `decimalai skills update`, and the activation says so. Restart the process to serve the new one.
* Without `enable_sensitive_telemetry()`, the activation is still recorded, but the trace does not show what each tool call read or returned.
* A bad folder is skipped, with an ERROR line on the `agent_framework` logger.

Graded at agent-framework-core 1.21.0.

## Pydantic AI harness

```python theme={null}
import decimalai
import decimalai.pydantic_ai_harness
from pydantic_ai import Agent
from pydantic_ai.capabilities import LocalWorkspace
from pydantic_ai_harness import FileSystem, Shell

decimalai.init(otel=True, agent_name="support")
Agent.instrument_all()               # Pydantic AI's own spans: one trace per agent run

project = decimalai.skills_dir().parents[1]
agent = Agent(model, capabilities=[
    LocalWorkspace(project),
    decimalai.pydantic_ai_harness.skills(project),
    FileSystem(),
    Shell(),
])
agent.run_sync("Refund for $100 after 45 days?")
```

* `decimalai.pydantic_ai_harness.skills(project)` is the harness's `Skills` over your skills folder, relative to the workspace as the harness requires: a skills folder outside it still lists, but `read_file` refuses its files.
* No DecimalAI adapter is involved (SDK 0.19+). Don't also give this agent [`decimalai.pydantic_ai.agent_config()`](/sdk/python/frameworks/pydantic-ai)'s capability: the harness's catalog already serves its skills, so the capability adds neither DecimalAI's skill list nor `load_skill`, and one warning says so.
* The harness keeps a skill's name, description and body; `metadata` is parsed and dropped, so the version comes from the lock, by the hash of the delivered body.
* It keeps a user-only skill (`disable-model-invocation: true`) out of the model's catalog. It does not enforce `allowed-tools`, and says so on every run with a `UserWarning`: *Ignoring unsupported Agent Skill behavioral frontmatter fields*.
* A bad folder goes into the catalog's `skipped` list with a `UserWarning`, never an exception.

Graded at pydantic-ai-harness 0.57.0 with pydantic-ai-slim 2.57.0.

## Strands Agents

```python theme={null}
import decimalai
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell

decimalai.init(otel=True, agent_name="support")

agent = Agent(
    model=model,
    system_prompt="Answer refund questions.",
    tools=[file_editor, shell],
    plugins=[skills()],
)
agent("Refund for $100 after 45 days?")
```

* `skills()` is `AgentSkills(skills=<absolute skills folder>, strict=True)` with the observer attached, still the one plugin Strands knows as `agent_skills`. The `skills` tool answers with the body and a `Location:` line; the model then reads files with `file_editor` and runs scripts with `shell`. `file_editor` refuses relative paths, which is why the root is absolute.
* Strands puts tool arguments and results in span events, which the exporter does not read. Set `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental,gen_ai_span_attributes_only` before the first agent is built to have them on the trace; the activation is recorded either way.
* Strands loads an agent's skills once, so the same agent keeps serving the old version after `decimalai skills update`.
* **`strict=True` on the plugin does not raise**: it logs "failed to load skill" and skips that folder. `Skill.from_file(path, strict=True)` is the call that raises.
* Always pass `model=`. Strands' default model is Amazon Bedrock.

Graded at strands-agents 1.61.0.

## Known upstream gaps

Facts about the frameworks, not about the folders. Each is re-checked on every run of the SDK's test suite, so the line here goes the day the framework changes.

| Gap | Loaders | What to do |
| - | - | - |
| `disable-model-invocation: true` is ignored: a user-only skill is offered to the model | Every loader except Claude Code and the Pydantic AI harness | Don't install a user-only skill for an agent on these frameworks unless the model may use it |
| The exec bit is dropped when a skill is copied into the sandbox | OpenAI Agents | Run scripts through their interpreter |
| No way to read a skill's bundled files or run its scripts | CrewAI | `decimalai.crewai.skill_tools()` |
| No script runner ships, and every skill tool waits for approval | Microsoft Agent Framework | `decimalai.agent_framework.skills()` |
| Skills are cached after the first load | Google ADK, Microsoft Agent Framework, Strands | Rebuild the agent (or restart) after `decimalai skills update`; the activation names the version actually served until then |
| An unknown skill name aborts the run; a symlinked skill folder is skipped silently | OpenAI Agents | `decimalai.openai_agents.sandbox_skills()` |
| One bad folder fails the whole load | Google ADK | `decimalai.adk.skills()` |
| `allowed-tools` is not enforced (a `UserWarning` on every run says so) | Pydantic AI harness | Grant tools in your agent's own configuration |
| `metadata` (the identity stamp) is not exposed | Claude Code, Pydantic AI harness | Nothing: DecimalAI finds the version by the body's hash |

## What a loader does with a bad folder

Measured one defect at a time, with every loader at the version graded above. Most of them do not raise:

* **Google ADK raises** for a name or a list-typed field that breaks the format, and then loads nothing at all (`decimalai.adk.skills()` skips that folder instead).
* **Microsoft Agent Framework, CrewAI, Strands and the Pydantic AI harness skip the folder** and log a warning, which is easy to miss.
* **OpenAI Agents and Deep Agents rename**: a namespaced or uppercase `name:` is listed under that name instead of the folder's. OpenAI Agents also skips a symlinked skill folder without a word.
* **Claude Code** lists a skill whose `name:` differs from its folder under the other name.

So a broken folder usually shows up as a skill the agent never uses. [`decimalai skills verify`](/guides/skills-on-disk#status-and-verify) reports these defects before a loader drops them, and the folders `decimalai skills add` writes have none of them.

## How the SDK checks this

On every commit, the SDK's test suite starts each of the eight loaders in a child process of its own, points it at a project in the format `decimalai skills add` writes, and drives it with a scripted model.

**That the loaders read the folders.** One scenario per loader (load a skill, read one of its bundled files, run its script, answer), checking that:

* the loader lists exactly the installed skills, under their install names;
* its own warning channel stays quiet;
* the model receives the skill's own body, and its hash equals the stamp;
* a bundled file reads back byte for byte, and a bundled script runs and prints what it should, through the loader's own tools;
* the identity stamp reads back and equals the lock;
* user-only skills are hidden (Claude Code, the Pydantic AI harness), or the upstream gap above holds;
* the script keeps its exec bit in the copy the loader runs.

**That DecimalAI sees them do it.** With DecimalAI's instrumentation on and the helper on this page, each framework runs once: it lists, loads a skill twice, reads its resource and runs its script. Then `decimalai skills update` moves the skill to a new version, and the same agent loads it again. The suite checks the traces DecimalAI sends for:

* the listing;
* one activation carrying the installed version's exact ids;
* the resource read and the script run;
* after the update, the version whose body the model actually received.

It runs at each framework's lowest supported version too.

## Without a helper

The DecimalAI adapters for the Claude Agent SDK, Google ADK, CrewAI, OpenAI Agents and Deep Agents watch the framework's own loader however you set it up, so the plain snippets from those frameworks' docs are recorded too. For Microsoft Agent Framework, Strands and the Pydantic AI harness, the observer lives on the object the helper returns.

Anywhere else, record a load yourself inside a trace. Name the skill as the loader shows it, by its install name, `decimalai-skills:<install-name>` or its folder path. Before the trace is sent, the SDK looks the name up in the lock and adds the registry name, skill id, version id and body hash. It hashes the folder as it is at that moment, though, so after a `skills update` a loader that cached the old version is credited with the new one. The helpers above do not have that problem.

```python theme={null}
import decimalai

with decimalai.start_trace(agent_name="support"):
    ...  # your agent run
    decimalai.log_skill_activation(name="acme-refund-policy")
```

See [Skills in the SDK](/sdk/python/skills) for the trace calls, and [Skills on disk](/guides/skills-on-disk#how-usage-is-counted) for how installs and loads are counted.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.