Claude Agent SDKclaude-agent-sdk 0.2.157 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

Commands

A command is anything a session runs when you send /name as the prompt. Dispatch goes by name alone, so it does not depend on the skills option.

Last updated: 20 Oct, 2026 · Claude Agent SDK

A skill is normally picked by Claude. The same skill can be run on demand instead, by sending its name, which is how a reusable workflow gets a handle you can call.

What a session answers to

  • Built-in commands that run inside the Claude Code process, such as /compact and /clear.
  • Bundled skills that ship with Claude Code, such as /code-review.
  • Your skills. A skill that can be invoked by a person joins the list by itself.
  • Command files, flat Markdown files in .claude/commands/ whose filename is the command name. Skills are their successor, and both keep working.

The full list for a session is in the init message's slash_commands field: skills.

Adding a command file

A command file has no frontmatter to fill in. The filename is the name, and the text is the instruction. $ARGUMENTS is replaced with whatever follows the name in the prompt.

Example.claude/commands/order-note.md
Say exactly: Noted on order $ARGUMENTS and passed to the warehouse.

shop_desk.py gains command_files and dispatch, and start and turn are both replaced to use them.

python
from claude_agent_sdk import ClaudeAgentOptions
from shop_desk import ShopDesk

# the folder is read at startup, so a desk built here already knows the names
desk = ShopDesk(ClaudeAgentOptions(setting_sources=["project"]))
print(sorted(desk.command_files()))
Exampleshop_desk.py, a new method
    def command_files(self):
        """name -> text for every .claude/commands/*.md file this session loads."""
        return {prefix + path.stem: path.read_text()
                for root, prefix in self.roots()
                for path in sorted(root.glob("commands/*.md"))}
Exampleshop_desk.py, a second new method
    def dispatch(self, prompt):
        """A /name prompt: whatever carries that name, found by name alone."""
        name, _, args = prompt[1:].partition(" ")
        body = self.found.get(name) or self.command_files().get(name)
        if body is not None:
            return {"text": self.follow(body, args)}

        # a /name that matches nothing is passed on as text, with a note, and costs a turn
        self.said[-1] = f"{prompt} (this command did not run)"
        return self.think([])
Exampleshop_desk.py, turn
    async def turn(self, prompt):
        await self.start(prompt)

        # a /name prompt names the work itself, so the first step needs no model
        step = self.dispatch(prompt) if prompt.startswith("/") else self.think([])
        results, turns, limit = [], 0, self.options.max_turns
        while "calls" in step:
            turns += 1
            if limit and turns > limit:
                message = f"Reached maximum number of turns ({limit})"
                return await self.finish("error_max_turns", turns, message)
            results = [await self.use(name, args) for name, args in step["calls"]]
            step = self.think(results)

        await self.say(step["text"])
        await self.finish("success", turns + 1, step["text"])

dispatch looks in self.found, the skills the session discovered, before the command files. It never looks at permitted, which is what makes the next run work.

Running a skill the allowlist leaves out

python
from claude_agent_sdk import ClaudeAgentOptions
from shop_tools import shop
from watch import watch

# skills are off for this session, which is what makes the dispatch worth watching
options = ClaudeAgentOptions(mcp_servers={"shop": shop}, setting_sources=["project"],
                             allowed_tools=["mcp__shop__lookup_order"], skills=[])
Example
await watch("/refund-policy", options)

Same session, same empty skills list that left Claude with no Skill tool in the skills lesson. Sending the name runs the skill anyway, because dispatch and the allowlist are two different gates.

Passing an argument to a command file

Example
await watch("/order-note A17", options)

Everything after the name is the argument, and $ARGUMENTS in the file is where it lands. The documentation also lists $0 and $1 for single words.

Sending a name that matches nothing

Example
await watch("/discount A17", options)
  • The query does not fail. Claude Code passes the prompt on as an ordinary message with a note that the command did not run.
  • A model turn is spent. Here the desk reads the order id out of the text and looks it up.
  • A built-in that needs a terminal, such as /theme, answers that it is not available in this environment and spends no turn.

Listing what this session answers to

Example
from claude_agent_sdk import SystemMessage, query
from shop_desk import ShopDesk

async for message in query(prompt="Hello", options=options, transport=ShopDesk(options)):
    if isinstance(message, SystemMessage) and message.subtype == "init":
        print(message.data["slash_commands"])

A real session's list is longer, because the built-ins and the bundled skills are in it too. The desk lists only what the desk itself can run, and the built-ins stay with the real Claude Code process: /compact summarises the history it has, and /clear empties it.

Skill, command file and built-in

KindWhere it livesWho can start it
Skill.claude/skills/<name>/SKILL.mdClaude, when the description matches, and you, by name
Command file.claude/commands/<name>.mdYou, by name
Built-inInside the Claude Code processYou, by name

Where dispatch by name is the right call

  • A release or review workflow a person triggers, rather than one Claude decides to run.
  • A fixed step in your own application, where a button sends /name as the prompt.
  • Compacting a long conversation before the next question, with /compact.
Watch out. A command is a prompt like any other, so it counts against max_turns and can end the query with an error result instead of success. Wrap the loop in try when a command might run long.
Try it yourself
  • Rename the file to order-note-2.md and send the old name. What comes back?
  • Send /order-note with no argument and read what $ARGUMENTS becomes.
  • Add max_turns=1 to the options and send /discount A17 again.
PreviousSkills

Slow is fine. Stopping is the only problem.