markdown.engineering
Lesson 03

The Skills System

How Claude Code discovers, loads, parses, and executes reusable prompt workflows

1. What Are Skills?

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.

Why skills instead of plain prompts? Skills are versioned, shareable (check them into your repo), discoverable (Claude sees the skill menu and auto-invokes), and composable (a skill can invoke other skills). They also carry permission hints, model overrides, and lifecycle hooks.
Slash commands SKILL.md files Bundled into CLI Served via MCP

2. Skill Lifecycle

Every skill travels through six stages from the moment Claude Code starts to the moment Claude acts on it.

flowchart LR D["Discovery
Walk managed / user /
project / --add-dir paths.
Read dir entries.
Resolve symlinks."] L["Load
Read SKILL.md.
Parse YAML frontmatter.
Estimate token budget."] P["Parse
Extract all frontmatter
fields into Command
object. Validate hooks,
paths, effort, shell."] S["Substitute
Replace $ARGUMENTS,
$1/$name, shell !backtick,
${CLAUDE_SKILL_DIR},
${CLAUDE_SESSION_ID}."] E["Execute
Run inline (expand into
conversation) or fork
(isolated sub-agent with
own token budget)."] I["Inject
Tool result enters
conversation. Allowed
tools and model override
applied for this turn."] D --> L --> P --> S --> E --> I style D fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style L fill:#22201d,stroke:#7d9ab8,color:#b8b0a4 style P fill:#22201d,stroke:#6e9468,color:#b8b0a4 style S fill:#22201d,stroke:#c47a50,color:#b8b0a4 style E fill:#22201d,stroke:#8e82ad,color:#b8b0a4 style I fill:#22201d,stroke:#6e9468,color:#b8b0a4

Discovery

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.

Load

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.

Parse

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.

Substitute

When invoked, the skill body undergoes argument substitution in this order:

  1. Named args: $foo, $bar (mapped by position from arguments frontmatter)
  2. Indexed args: $ARGUMENTS[0], $0, $1
  3. Full arg string: $ARGUMENTS
  4. If no placeholder was found and args exist, they are appended as ARGUMENTS: ...
  5. Shell injection: !`command` or ```! blocks (local skills only — MCP skills are blocked)
  6. Special vars: ${CLAUDE_SKILL_DIR}, ${CLAUDE_SESSION_ID}

Execute

Skills run in one of two modes determined by context: fork in frontmatter:

  • Inline (default): The expanded prompt is injected into the current conversation as a user message. Claude processes it in the same context window.
  • Forked: The skill runs in an isolated sub-agent (runAgent()) with its own token budget. The parent conversation receives only the final text output. Ideal for self-contained tasks.

Inject

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.

3. Four Skill Sources & Priority

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.

Priority 1

Managed / Policy

managed/.claude/skills/

Enterprise-controlled skills deployed by IT. Can be locked to prevent user override via CLAUDE_CODE_DISABLE_POLICY_SKILLS.

Priority 2

User (Personal)

~/.claude/skills/

Your cross-project personal skill library. Available everywhere you run Claude Code. Watched for live changes via chokidar.

Priority 3

Project

.claude/skills/

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.

Priority 4

Bundled

compiled into CLI

Skills like /simplify, /loop, /remember that ship with the binary. Registered via registerBundledSkill() at startup. Some are feature-flagged.

Highest priority Managed User Project Bundled Lowest priority

A fifth source: MCP skills

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.

Legacy: .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/.

Live reloading

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.

4. SKILL.md Format & Frontmatter

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).

--- name: "My Workflow" # Display name (overrides dir name) description: "One-line summary" # Shown in /skills menu + budget listing when_to_use: "Use when X. Examples: ..." # Auto-invoke hint for Claude allowed-tools: # Minimum permissions (additive) - Bash(gh:*) - Read - Write argument-hint: "[branch] [message]" # Shown in autocomplete UI arguments: branch message # Named args → $branch, $message context: fork # 'fork' = isolated sub-agent; omit for inline model: claude-opus-4-5 # Override model for this skill only effort: high # low | medium | high | integer version: "1.0.0" # Arbitrary version string (informational) user-invocable: true # false → hidden from /skills menu paths: src/payments/** # Conditional: only active for matching files hooks: # Pre/post lifecycle hooks PreToolUse: - matcher: ... agent: code # Agent type for forked execution shell: # Shell config for !backtick injection interpreter: bash --- # My Workflow Body text. Use $branch and $message for named arg substitution. Or $ARGUMENTS for the raw arg string. Or inline shell: !`git log --oneline -5`

Field reference

FieldTypeDescription
namestringOverride the directory-derived display name
descriptionstringOne-line summary shown in skill listing. Falls back to first paragraph of body.
when_to_usestringDetailed trigger instructions for Claude. Combined with description in the listing.
allowed-toolslistTool permission patterns granted during this skill. Use narrowest patterns: Bash(gh:*) not Bash.
argument-hintstringShown in the CLI autocomplete as a placeholder hint.
argumentsstring or listNamed argument identifiers. Maps positionally to $name substitutions in body.
contextforkRuns the skill as an isolated sub-agent. Omit for inline (default).
modelstringModel alias to use when executing this skill. inherit means use the session default.
effortlow/medium/high/intThinking budget applied when running this skill.
versionstringInformational version tag; no runtime effect.
user-invocableboolDefault true. Set false to hide from /skills menu (agent-only skills).
pathsglob string(s)Conditional activation — skill appears only when a matching file is opened.
hooksobjectPre/post tool-use lifecycle hooks. Validated with HooksSchema at load time.
agentstringAgent type identifier used when forking. E.g. code, browser.
shellobjectShell interpreter config for !backtick execution inside the skill body.
disable-model-invocationboolIf true, the skill cannot be called via the Skill tool (only via slash command).

5. Advanced Skill Patterns

Conditional Skills (paths-based activation)

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 (compiled into the CLI)

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 Skills (served over the Model Context Protocol)

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:

  • No shell injection: !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."
  • ${CLAUDE_SKILL_DIR} is meaningless: MCP skills have no local directory, so this variable is not substituted.
  • Registered separately: MCP skills live in AppState.mcp.commands, not the local skill registry. The Skill tool merges them via getAllCommands() at invocation time.
  • The registry bridge: 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')
Skill Permission & Auto-Allow Logic

Every Skill tool invocation goes through checkPermissions(). The decision follows this waterfall:

  1. Deny rules: If a deny rule matches the skill name (or prefix with :*), block immediately.
  2. Remote canonical skills: Auto-allowed (ant-only experimental feature).
  3. Allow rules: If an explicit allow rule matches, proceed without asking.
  4. Safe properties check: If the skill only uses "safe" properties (no allowed-tools, no model override, no hooks, no paths, etc.), it is auto-allowed without prompting the user.
  5. Ask: Otherwise, the user is prompted with suggestions to add exact or prefix (:*) 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 Substitution in Depth

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:

PatternWhat it resolves to
$foo, $barNamed arg by position (requires arguments: foo bar frontmatter)
$ARGUMENTS[0], $0Indexed positional arg
$ARGUMENTSFull 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.

6. Key Takeaways

  • 📁 Skills live at <skill-name>/SKILL.md. The directory name is the slash command. YAML frontmatter controls all metadata; the Markdown body is the prompt.
  • 🔍 Claude discovers skills from four sources in priority order: managed → user → project → bundled. MCP skills are merged separately at invocation time.
  • Skills run inline (default, same conversation context) or forked (context: fork, isolated sub-agent). Fork is better for self-contained tasks; inline is better when you need mid-process steering.
  • 🔒 MCP skills never execute shell injection. Only local (managed/user/project/bundled) skills can run !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.
  • 💡 The skill listing is budget-capped at 1% of the context window. Bundled skill descriptions are never truncated; custom skill descriptions may be shortened if the listing exceeds budget.
  • 🔄 Skill files are watched live. Saving a SKILL.md triggers a debounced reload (300 ms). No restart needed.
  • Simple skills (no allowed-tools, no model override) are auto-approved without any permission prompt. More powerful skills require explicit user allow rules.

7. Quiz

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?

A The project skill — project always wins
B The user skill — user source has higher priority than project
C Both — they are merged into one skill
D Neither — name collision causes an error

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?

A agent: isolated
B user-invocable: false
C context: fork
D model: inherit

Q3 — An MCP server sends a SKILL.md that contains !`rm -rf /tmp/cache`. What happens?

A Claude Code asks for permission before executing
B The shell block is silently skipped — MCP skills never execute inline shell
C It runs if the skill has Bash in allowed-tools
D The skill fails to load

Q4 — You add paths: src/payments/** to your skill's frontmatter. When does this skill become visible to Claude?

A Always — paths is just a tag for documentation
B Only when the user manually types /skills
C When a file matching src/payments/** is opened or edited in the session
D On the next Claude Code restart

Q5 — Your SKILL.md body contains no $ARGUMENTS placeholder. The user runs /myskill feature-123. What happens to the argument?

A It is silently discarded
B An error is thrown: "no placeholder found"
C It is appended to the end of the prompt as ARGUMENTS: feature-123
D It becomes $0 automatically

Q6 — You have 200 custom skills. The total listing exceeds 1% of the context window budget. Which skills' descriptions are NEVER truncated?

A The most recently used skills
B Bundled skills that ship with the CLI binary
C Skills with a when_to_use field set
D Skills from the managed/policy source