How Claude Code discovers, loads, parses, and executes reusable prompt workflows
A skill is a named, reusable prompt workflow that Claude Code can discover and execute. Skills are stored as SKILL.md Markdown files with a YAML frontmatter header, or compiled directly into the CLI binary as bundled skills.
From the user's perspective, a skill is a slash command: /commit, /simplify, or /review-pr. From Claude's perspective, invoking a skill calls the Skill tool (Skill), which loads the full prompt, substitutes any arguments, optionally executes inline shell blocks, then injects the result into the conversation.
Every skill travels through six stages from the moment Claude Code starts to the moment Claude acts on it.
At startup getSkillDirCommands() walks four locations in parallel and also loads legacy .claude/commands/ directories. Symlinks are resolved via realpath() so duplicate files (same content, different paths) are deduplicated before any skill reaches Claude.
Each skill-name/SKILL.md file is read. Token count is estimated from frontmatter only (name + description + whenToUse) — the full body is not tokenised at startup. This keeps the skill listing fast even with hundreds of skills. The listing itself is budget-capped at 1% of the context window.
The frontmatter is parsed into a Command object. All fields (description, allowed tools, argument hints, model override, hooks, paths, effort, shell) are validated here. Skills with a paths field become conditional skills — they are stored but not surfaced to Claude until the user opens a matching file.
When invoked, the skill body undergoes argument substitution in this order:
$foo, $bar (mapped by position from arguments frontmatter)$ARGUMENTS[0], $0, $1$ARGUMENTSARGUMENTS: ...!`command` or ```! blocks (local skills only — MCP skills are blocked)${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID}Skills run in one of two modes determined by context: fork in frontmatter:
runAgent()) with its own token budget. The parent conversation receives only the final text output. Ideal for self-contained tasks.The Skill tool returns a ToolResult. For inline skills: the result carries allowedTools and an optional model override that apply to subsequent tool calls in this turn. For forked skills: a Done byline is displayed and the sub-agent's output feeds back as context.
Skills can originate from four distinct sources. When two sources define a skill with the same name, the first one loaded wins (managed > user > project > bundled). Deduplication is by resolved file path, not by name, so a symlinked skill can shadow a real one.
Enterprise-controlled skills deployed by IT. Can be locked to prevent user override via CLAUDE_CODE_DISABLE_POLICY_SKILLS.
Your cross-project personal skill library. Available everywhere you run Claude Code. Watched for live changes via chokidar.
Repository-scoped skills checked into the repo. Claude walks up from the project root, so nested .claude/skills/ directories are discovered dynamically as files are opened.
Skills like /simplify, /loop, /remember that ship with the binary. Registered via registerBundledSkill() at startup. Some are feature-flagged.
When an MCP server exposes skills (detected by loadedFrom === 'mcp'), they are fetched at connection time and added to the command registry. MCP skills follow the same frontmatter schema but shell injection is blocked — !backtick and ```! blocks are never executed for remote, untrusted content.
.claude/commands/The older /commands/ directory is still supported (loadedFrom: 'commands_DEPRECATED'). It accepts both single .md files and the skill-name/SKILL.md directory format. New work should use .claude/skills/.
The skillChangeDetector module watches skill directories with chokidar. When any SKILL.md changes, it debounces (300 ms), fires ConfigChange hooks, then clears all memoization caches so the next invocation sees fresh skills. On Bun, stat-polling is used instead of FSWatcher to avoid a known Bun deadlock bug.
A skill file is plain Markdown with an optional YAML frontmatter block delimited by ---. The file must live at <skill-name>/SKILL.md (the directory name becomes the slash command name).
| Field | Type | Description |
|---|---|---|
name | string | Override the directory-derived display name |
description | string | One-line summary shown in skill listing. Falls back to first paragraph of body. |
when_to_use | string | Detailed trigger instructions for Claude. Combined with description in the listing. |
allowed-tools | list | Tool permission patterns granted during this skill. Use narrowest patterns: Bash(gh:*) not Bash. |
argument-hint | string | Shown in the CLI autocomplete as a placeholder hint. |
arguments | string or list | Named argument identifiers. Maps positionally to $name substitutions in body. |
context | fork | Runs the skill as an isolated sub-agent. Omit for inline (default). |
model | string | Model alias to use when executing this skill. inherit means use the session default. |
effort | low/medium/high/int | Thinking budget applied when running this skill. |
version | string | Informational version tag; no runtime effect. |
user-invocable | bool | Default true. Set false to hide from /skills menu (agent-only skills). |
paths | glob string(s) | Conditional activation — skill appears only when a matching file is opened. |
hooks | object | Pre/post tool-use lifecycle hooks. Validated with HooksSchema at load time. |
agent | string | Agent type identifier used when forking. E.g. code, browser. |
shell | object | Shell interpreter config for !backtick execution inside the skill body. |
disable-model-invocation | bool | If true, the skill cannot be called via the Skill tool (only via slash command). |
A skill with a paths frontmatter field is a conditional skill. It is loaded at startup but not surfaced to Claude until the user opens or edits a file whose path matches one of the glob patterns.
This allows you to keep payment-specific, infra-specific, or iOS-specific workflows invisible until they are actually relevant — avoiding noise in the skill listing for unrelated work.
--- paths: src/payments/** description: "Stripe refund workflow" --- # Stripe Refund Steps to issue a refund via the Stripe API...
Patterns use the same syntax as .gitignore / CLAUDE.md conditional rules. A pattern of ** (match-all) is treated as unconditional (same as omitting paths).
Conditional skills also interact with dynamic skill discovery: as the model reads files deeper in the project tree, Claude Code walks up toward cwd and may discover additional .claude/skills/ directories not found at startup. Gitignored directories are skipped.
Bundled skills ship inside the Claude Code binary and are registered at startup via registerBundledSkill(). They follow the same BundledSkillDefinition interface as file-based skills but provide a getPromptForCommand(args, context) function instead of a SKILL.md file.
Well-known bundled skills include:
/simplify — spawns three parallel review agents (reuse, quality, efficiency)/loop — parses an interval + prompt and creates a cron job (feature-flagged)/remember, /verify, /debug, /stuck/skillify — interviews you about the current session and writes a SKILL.md (Anthropic-internal)Bundled skills can also include reference files via the files property. These are extracted to a per-process nonce directory on first invocation (using O_EXCL | O_NOFOLLOW | 0o600 flags to prevent symlink attacks), and a Base directory for this skill: ... prefix is prepended to the prompt so Claude can Read/Grep them.
Some bundled skills are conditional on feature flags (feature('AGENT_TRIGGERS'), feature('KAIROS')) or runtime checks (isKairosCronEnabled()). They can be added or removed without modifying SKILL.md on disk.
// Registering a bundled skill
registerBundledSkill({
name: 'simplify',
description: 'Review changed code for reuse, quality, and efficiency.',
userInvocable: true,
async getPromptForCommand(args) {
return [{ type: 'text', text: SIMPLIFY_PROMPT }]
},
})
MCP servers can expose skills in addition to tools. When Claude Code connects to an MCP server with the MCP_SKILLS feature enabled, it calls fetchMcpSkillsForClient(), which parses the skill frontmatter using the same parseSkillFrontmatterFields() and createSkillCommand() functions used for file-based skills.
MCP skills appear in the SkillsMenu under their own "MCP skills" group. Their names follow the convention server-name:skill-name.
Key differences from local skills:
!backtick and ```! blocks are silently skipped. The code comment says: "Security: MCP skills are remote and untrusted — never execute inline shell commands from their markdown body."AppState.mcp.commands, not the local skill registry. The Skill tool merges them via getAllCommands() at invocation time.mcpSkillBuilders.ts is a dependency-graph leaf module that holds references to createSkillCommand and parseSkillFrontmatterFields, solving a circular import problem between client.ts → mcpSkills.ts → loadSkillsDir.ts.// How getAllCommands() merges MCP and local skills const mcpSkills = context.getAppState().mcp.commands .filter(cmd => cmd.type === 'prompt' && cmd.loadedFrom === 'mcp') const localCommands = await getCommands(getProjectRoot()) return uniqBy([...localCommands, ...mcpSkills], 'name')
Every Skill tool invocation goes through checkPermissions(). The decision follows this waterfall:
deny rule matches the skill name (or prefix with :*), block immediately.allow rule matches, proceed without asking.:*) allow rules to local settings.This means simple informational skills run without any permission dialog, while skills that gain Bash access or override the model must be explicitly approved.
Argument parsing uses shell-quote so quoted strings work as single tokens:
/myskill "hello world" foo → ["hello world", "foo"]
Three substitution patterns are supported, processed in order:
| Pattern | What it resolves to |
|---|---|
$foo, $bar | Named arg by position (requires arguments: foo bar frontmatter) |
$ARGUMENTS[0], $0 | Indexed positional arg |
$ARGUMENTS | Full raw argument string |
If the skill body contains no placeholder and the user provided args, they are appended automatically: ARGUMENTS: <args>. This prevents args from being silently dropped in simple skills that don't declare placeholders.
<skill-name>/SKILL.md. The directory name is the slash command. YAML frontmatter controls all metadata; the Markdown body is the prompt.
context: fork, isolated sub-agent). Fork is better for self-contained tasks; inline is better when you need mid-process steering.
!backtick blocks. This is a hard security boundary in the source code.
paths frontmatter makes a skill conditional — invisible until a matching file is opened. Use this to surface domain-specific workflows only when they are relevant.
Q1 — You have a skill in ~/.claude/skills/deploy/SKILL.md and another in .claude/skills/deploy/SKILL.md (the current project). Which one does Claude use?
Q2 — You want a skill that runs database migration scripts in an isolated context without sharing the conversation history. Which frontmatter field do you set?
Q3 — An MCP server sends a SKILL.md that contains !`rm -rf /tmp/cache`. What happens?
Q4 — You add paths: src/payments/** to your skill's frontmatter. When does this skill become visible to Claude?
Q5 — Your SKILL.md body contains no $ARGUMENTS placeholder. The user runs /myskill feature-123. What happens to the argument?
Q6 — You have 200 custom skills. The total listing exceeds 1% of the context window budget. Which skills' descriptions are NEVER truncated?