Skills in the SDK
Load SKILL.md skills into Agent SDK sessions, limit which ones Claude may use, dispatch slash commands by name and fix skills that will not load.
Skills are folders containing a SKILL.md file: a description of when to use them, instructions, and optionally scripts or reference files. In the Agent SDK they work much as they do in the CLI, with one important difference: you cannot register a skill in code. Skills are always files on disk, and the SDK finds them through its setting sources.
This page also covers commands, since sending /<name> in a prompt is how you run a skill (or a built-in such as /compact) directly from your code.
How the SDK handles skills
- They are files. Each skill is a directory such as
.claude/skills/invoice-check/SKILL.md. There is no API to register one; contrast that with subagents, which you can define in theagentsoption. - Discovery follows setting sources. At startup the SDK reads skill metadata from the user and project locations. The full body only loads when the skill is used.
- Claude picks them. Claude decides when a skill is relevant, using its
description. - You can force one. Send
/<name>as the prompt. - You can scope them. The
skillsoption limits which ones Claude is allowed to invoke.
Where skills are found
With default query() options both the user and project sources load, which gives you:
| Location | Source |
|---|---|
~/.claude/skills/ | user |
<cwd>/.claude/skills/ | project |
.claude/skills/ in each parent of cwd, up to the repository root | project |
<dir>/.claude/skills/ for each directory in additionalDirectories (TS) or add_dirs (Python) | project, because these are passed to Claude Code as --add-dir |
If you set settingSources / setting_sources yourself, include project and user to keep those skills. To load skills from an arbitrary path instead, package them as a plugin and use the plugins option.
Older custom command files in .claude/commands/ and ~/.claude/commands/ load from the same two scopes. A file at .claude/commands/release.md behaves like a skill at .claude/skills/release/SKILL.md. Skills are the recommended form going forward. See skills for which one wins when names collide.
The skills option
| Value | Effect |
|---|---|
| omitted | Every discovered skill is enabled and the Skill tool is available, matching the CLI |
"all" | Same: every discovered skill |
["name", ...] | Only these skills can be invoked by Claude |
[] | Claude can invoke none |
When you set skills, the SDK adds the Skill tool to allowedTools for you. If you also pass an explicit tools list, add "Skill" to it, or Claude has no way to call a skill.
Here is an agent for a finance team that should only ever use two in-house skills:
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const msg of query({
prompt: "Check last month's supplier invoices against the purchase orders",
options: {
cwd: "/srv/finance-agent",
settingSources: ["project"],
skills: ["invoice-check", "po-lookup"],
allowedTools: ["Read", "Glob", "Bash(python scripts/*)"]
}
})) {
if (msg.type === "result") console.log(msg.subtype, msg.subtype === "success" ? msg.result : "");
}
from claude_agent_sdk import ClaudeAgentOptions
opts = ClaudeAgentOptions(
cwd="/srv/finance-agent",
setting_sources=["project"],
skills=["invoice-check", "po-lookup"],
allowed_tools=["Read", "Glob", "Bash(python scripts/*)"],
)
Rules for the allowlist
- Names match the
nameinSKILL.md, or the directory name. Plugin skills useplugin:skill. - Exact names only. Wildcards are rejected: use
"all"rather than*. - Unlisted skills are hidden from the model and the Skill tool refuses them. Their files are still on disk, so Claude could read them with Read or Bash if those tools are allowed.
- The allowlist does not affect dispatch by name. Sending
/<name>runs a user-invocable skill even if it is not listed.
Confirming what loaded
Early in the stream the SDK yields a system message with subtype init. Its skills array lists user-invocable skills that have a description or when_to_use field, plus the skills bundled with Claude Code. Skills with user-invocable: false still load and remain available to Claude but are left out of the array. The array is the same regardless of your skills list.
Writing a skill
Create a directory and a SKILL.md with YAML frontmatter. The description is what Claude matches against, so make it specific.
.claude/skills/invoice-check/
├── SKILL.md
└── rules.md
---
name: invoice-check
description: Validate supplier invoices against purchase orders. Use when asked to check, reconcile or audit invoices.
---
For each invoice in the given folder:
1. Find the matching purchase order number.
2. Compare line totals and VAT against the PO. Apply the tolerances in rules.md.
3. Report mismatches as a table: invoice, PO, field, expected, actual.
Put project skills in .claude/skills/ and personal ones in ~/.claude/skills/. The skills guide covers the full frontmatter, arguments ($ARGUMENTS, $0, $1), the ${CLAUDE_*} substitutions and !`command` lines that inject live output before Claude reads the skill.
Pre-approving tools a skill needs
A skill runs with the session's tools. To stop it pausing for approval, either list tools in the skill's allowed-tools frontmatter or pass them in allowedTools / allowed_tools. Both pre-approve; neither restricts the other tools. If your organisation sets allowManagedPermissionRulesOnly in managed settings, both are ignored. Skills synced from claude.ai follow their own frontmatter rules. The permissions page covers modes and canUseTool.
Commands in SDK sessions
A command is anything you trigger by sending /<name>. Four things sit behind that surface:
| Kind | Example | What it is |
|---|---|---|
| Built-in command | /compact | Logic inside the Claude Code process |
| Bundled skill | /code-review, /verify | Prompt files shipped with Claude Code |
| Your skill | /invoice-check | A user-invocable SKILL.md you wrote |
| Custom command file | /release | A Markdown file in .claude/commands/ |
By default both you and Claude can invoke any skill; frontmatter can restrict either path.
Note: A
.claude/commands/file named after a bundled skill (for examplecode-review.md) shadows the bundled one, and the name appears only once in the command list.
Listing available commands
The init message's slash_commands field lists every command usable without an interactive terminal. Terminal-only commands such as /theme and /terminal-setup are omitted, as are skills marked user-invocable: false.
from claude_agent_sdk import query, ClaudeAgentOptions, SystemMessage
async for msg in query(prompt="hi", options=ClaudeAgentOptions(max_turns=1)):
if isinstance(msg, SystemMessage) and msg.subtype == "init":
print(msg.data["slash_commands"])
In TypeScript, read message.slash_commands on the message where type === "system" and subtype === "init". Sessions with MCP servers may also expose MCP prompts as commands.
Running a command
Make the command the prompt string:
for await (const msg of query({ prompt: "/invoice-check ./inbox/september", options: { maxTurns: 15 } })) {
if (msg.type === "result" && msg.subtype === "success") console.log(msg.result);
}
What happens with unusual input:
/<name>that matches nothing: from v2.1.274 the text goes to Claude as an ordinary message with a note that no command ran, so it costs a turn. Earlier versions returnedUnknown command: /<name>with no model turn./<name>that is a built-in but unavailable here (such as/theme): the result is/theme isn't available in this environment.with no model turn.- A command can run out of
maxTurns/max_turnslike any prompt and end in an error result, so wrap the loop in try/catch or set the limit generously.
/compact
Summarises older history to free context. It needs an existing conversation, so use it on a continued or resumed session, or in streaming input mode:
async for msg in query(prompt="/compact",
options=ClaudeAgentOptions(continue_conversation=True, max_turns=1)):
if isinstance(msg, SystemMessage) and msg.subtype == "compact_boundary":
meta = msg.data["compact_metadata"]
print("Compacted from", meta["pre_tokens"], "tokens, trigger:", meta["trigger"])
In TypeScript the option is continue: true and the metadata is message.compact_metadata. A compact_boundary message only appears when compaction actually ran. If there was nothing to summarise, the run still ends in success and the result text explains why, for example Not enough messages to compact.
/clear
Resets to an empty context. The old conversation stays on disk and you can return to it with the resume option. It is useful in streaming input mode; for one-shot query() calls every call already starts empty, so just start a new query.
Troubleshooting
Skills are not found
- Setting sources.
setting_sources=[]loads no skills. Includeuserandproject. - Working directory. Skills load from
.claude/skills/incwdand its parents up to the repo root. Make surecwdis at or below the folder that holds them. - Files exist. Check with
ls .claude/skills/*/SKILL.mdandls ~/.claude/skills/*/SKILL.md.
The Claude Code features page tabulates exactly what each source loads.
Claude does not use the skill
- If you passed a list, check the name is in it. Otherwise the Skill tool returns
Skill <name> is not in this session's skills allowlist. Add it, or dispatch with/<name>. - Tighten the description: specific verbs and nouns that match how people phrase the request.
"Invalid skill name"
query() validates the skills list before launching Claude Code and rejects:
- empty names;
- names containing parentheses, commas or control characters;
- names with leading or trailing whitespace;
- wildcards such as
*or a:*suffix.
TypeScript throws an Error (from SDK 0.3.221), for example Invalid skill name "docs:*": wildcard-suffix names are not allowed; list each skill by its exact name. Python raises ValueError with the same wording (from SDK 0.2.129). An empty name gives Skill names must be non-empty strings.
For YAML errors and other general problems, see skills.