Skip to content

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 the agents option.
  • 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 skills option 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:

LocationSource
~/.claude/skills/user
<cwd>/.claude/skills/project
.claude/skills/ in each parent of cwd, up to the repository rootproject
<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

ValueEffect
omittedEvery 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 name in SKILL.md, or the directory name. Plugin skills use plugin: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:

KindExampleWhat it is
Built-in command/compactLogic inside the Claude Code process
Bundled skill/code-review, /verifyPrompt files shipped with Claude Code
Your skill/invoice-checkA user-invocable SKILL.md you wrote
Custom command file/releaseA 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 example code-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 returned Unknown 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_turns like 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

  1. Setting sources. setting_sources=[] loads no skills. Include user and project.
  2. Working directory. Skills load from .claude/skills/ in cwd and its parents up to the repo root. Make sure cwd is at or below the folder that holds them.
  3. Files exist. Check with ls .claude/skills/*/SKILL.md and ls ~/.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.