Skills are one of the most powerful — and most misunderstood — extension mechanisms in Claude Code. They're not plugins. They're not tools. They're not prompts. They're folders of instructions that Claude loads only when relevant, teaching it how to do specialized tasks without bloating the context window.
By the end of this guide, you'll understand what skills are, how they actually work under the hood, and how to build your own.
What You'll Learn
- The core idea: Skills give Claude knowledge and playbooks, not new actions
- The three-tier loading system: How skills stay cheap on context until they're needed
- The secret: Skills are just regular tool calls dressed up in markdown
- Subagents and
context: fork: How skills can run in isolated mini-conversations - Building your own: A full DIY implementation with any LLM
Skills vs Tools: A Mental Model
The clearest way to frame this:
- Tools/MCP give Claude new actions — call an API, query a database, run code
- Skills give Claude new knowledge and playbooks — how to do a task, what conventions to follow, domain expertise
MCP gives Claude hands. Skills give Claude a handbook.
A skill is just a folder with a SKILL.md file — plain markdown with a bit of YAML frontmatter. No code required (though you can bundle scripts). Claude loads the relevant skill into context when needed, then follows those instructions using the tools it already has.
The Three-Tier Lazy Loading System
This is the part that makes skills actually useful at scale. Not everything loads at once.
Tier 1 — Always in context: name + description only (tiny) ↓ triggered (user types /name OR Claude decides it's relevant) Tier 2 — Loaded on demand: full SKILL.md body (the instructions) ↓ if needed Tier 3 — Read when relevant: supporting files (reference.md, examples.md, etc.)
Tier 1: The Menu
At session start, Claude Code scans all skill directories and loads just the frontmatter (name + description) of every skill. Claude sees a lightweight menu:
- fix-issue: "Fix a GitHub issue by number" - api-conventions: "API design patterns for this codebase" - deploy: "Deploy the application to production"
Tier 2: The Body
When something triggers the skill — either you type /fix-issue 123 or Claude auto-matches your request against descriptions — the full SKILL.md body loads into context.
Tier 3: Supporting Files
If the loaded SKILL.md references files like [reference.md](reference.md), Claude decides on its own whether to read them based on the task. Nothing magical — just Claude choosing to Read() a file.
This is why description quality matters so much. The description is the only thing Claude sees when deciding whether to pull in the rest. A vague description means Claude either misses the skill or triggers it at the wrong time.
Under the Hood: Skills Are Just Tool Calls
The three-tier loading might sound like a special mechanism, but it's actually plain old tool calling — the same mechanism powering Read(file_path) and Bash(command).
Claude Code registers a tool called Skill with a minimal schema:
{ "name": "Skill", "input": { "command": "string" } }
The tool's description contains an <available_skills> section built dynamically from all skill frontmatter. This is the "menu" Claude reads — just text inside a tool description:
<available_skills> - fix-issue: "Fix a GitHub issue by number" (project) - api-conventions: "API design patterns for this codebase" (user) - deploy: "Deploy the application to production" (project) </available_skills>
When Claude decides a skill is relevant, it emits a standard tool_use block:
{ "type": "tool_use", "name": "Skill", "input": { "command": "fix-issue" } }
Claude Code catches this, reads the SKILL.md, does preprocessing (argument substitution, command execution), and returns the rendered content as the tool_result. Claude continues working with the full instructions now in context.
Mapping the Three Tiers to Tool Calls
| Tier | What it really is |
|---|---|
| Tier 1 (descriptions) | Text injected into the Skill tool's description |
| Tier 2 (full skill) | A Skill("fix-issue") tool call returning SKILL.md as result |
| Tier 3 (supporting files) | Regular Read("reference.md") calls by Claude |
There is no magic. Skills are a convenience layer on top of the same tool-calling mechanism that powers everything else.
Anatomy of a Skill
my-skill/ ├── SKILL.md # Required — instructions + frontmatter ├── reference.md # Optional — detailed docs loaded on demand ├── examples/ │ └── sample.md # Optional — example outputs └── scripts/ └── helper.py # Optional — scripts Claude can execute
The SKILL.md File
Two parts: YAML frontmatter (metadata) and markdown body (instructions).
--- name: fix-issue description: Fix a GitHub issue by number disable-model-invocation: false user-invocable: true allowed-tools: Read, Grep, Bash --- Fix GitHub issue $ARGUMENTS: 1. Read the issue with `gh issue view $ARGUMENTS` 2. Understand the requirements 3. Implement the fix 4. Write tests 5. Create a commit
Frontmatter Fields
| Field | Required? | Description |
|---|---|---|
name | No | Becomes the /slash-command. Defaults to directory name. |
description | Recommended | Claude uses this to decide when to auto-load it. |
disable-model-invocation | No | true = only the user can trigger. Default: false. |
user-invocable | No | false = hidden from / menu. Default: true. |
allowed-tools | No | Tools Claude can use without asking permission. |
context | No | fork = run in an isolated subagent. |
agent | No | Which subagent type handles context: fork. |
model | No | Model override when skill is active. |
argument-hint | No | Autocomplete hint, e.g. [issue-number]. |
How allowed-tools Really Works
This is a common source of confusion. allowed-tools does not grant tool access — it skips the per-use approval prompt for tools Claude already has access to.
Final tool access = min(your permission settings, skill's allowed-tools)
If a skill lists a tool your permissions deny, the skill cannot override that. Permission deny always wins.
Invocation Control: Who Can Trigger What
Two boolean knobs control triggering:
| Setting | User invokes? | Claude invokes? | Use case |
|---|---|---|---|
| Defaults | Yes | Yes | General-purpose skills |
disable-model-invocation: true | Yes | No | Dangerous actions: /deploy, /send-email |
user-invocable: false | No | Yes | Background knowledge, coding conventions |
You don't want Claude auto-triggering a deploy because the code "looks ready." And you don't want users manually invoking "legacy system context" as a slash command — it's background knowledge Claude should absorb on its own.
Where Skills Live
Location determines scope:
| Location | Path | Applies to |
|---|---|---|
| Enterprise | Managed settings | All users in your org |
| Personal | ~/.claude/skills/<name>/SKILL.md | All your projects |
| Project | .claude/skills/<name>/SKILL.md | This project only |
| Plugin | <plugin>/skills/<name>/SKILL.md | Where plugin is enabled |
Priority on name collisions: enterprise > personal > project.
Nested .claude/skills/ directories in subdirectories are auto-discovered. If you're editing a file in packages/frontend/, Claude also checks packages/frontend/.claude/skills/.
Dynamic Context Injection: The Killer Feature
The \command`syntax (written with a leading!`) is a preprocessor. Before Claude ever sees the skill content, Claude Code:
- Scans the SKILL.md body for any
\...`` patterns - Executes each as a shell command on your machine
- Replaces the pattern with the command's stdout
- Sends the final rendered text to Claude
Claude never sees the commands. It just gets data.
Before and After
What you write in SKILL.md:
--- name: pr-summary description: Summarize a pull request context: fork --- ## PR context - Diff: !`gh pr diff` - Comments: !`gh pr view --comments` - Changed files: !`gh pr diff --name-only` Summarize this pull request.
What Claude actually receives after preprocessing:
## PR context - Diff: diff --git a/src/auth.ts b/src/auth.ts + if (token.expired()) { + throw new AuthError('Token expired'); + } - Comments: @alice: We need to handle expired tokens before middleware. @bob: Agreed, three incidents this month. - Changed files: src/auth.ts, src/middleware.ts, tests/auth.test.ts Summarize this pull request.
Why This Matters
Without dynamic context injection, Claude would:
- Start the skill
- Call Bash to run
gh pr diff→ wait - Call Bash to run
gh pr view --comments→ wait - Call Bash to run
gh pr diff --name-only→ wait - Finally start actual analysis
Three round-trips burned on data gathering. With \command``, the data is already there the moment Claude starts.
Caveat: This syntax is Claude Code-specific. Skills using it won't port cleanly to Cursor, Codex CLI, etc.
Subagents: Another Claude in Another Room
A subagent is another Claude instance running in its own context window. Its own system prompt, its own tools, its own conversation. When it finishes, it returns a summary to the parent.
Main conversation (you talking to Claude) | |-- delegates to --> Explore subagent (read-only, Haiku, fast) | \-- returns: "Found 3 relevant files: ..." | |-- delegates to --> code-reviewer subagent (custom) | \-- returns: "2 critical issues, 5 suggestions" | \-- continues with summaries, not raw output
How Subagents Actually Work
A subagent is not a subprocess or background terminal. It's a second agentic loop running inside the same Claude Code Node.js process.
Claude Code's core is a simple while loop:
1. Send messages to Anthropic API 2. API returns response (may include tool calls) 3. Claude Code executes those tools locally 4. Tool results fed back as the next message 5. Repeat until plain text response (no more tool calls)
When the main Claude delegates, it calls the Agent tool. Claude Code starts a second loop with a different system prompt, empty history, and a different tool allowlist — but the same local tool execution on your machine.
Key constraints:
- Both loops run in the same Node.js process — no OS-level isolation
- Subagents cannot spawn other subagents — the tree is one level deep
- At most one foreground subagent branch at a time
Built-in Subagents
| Subagent | Model | Tools | Purpose |
|---|---|---|---|
| Explore | Haiku | Read-only | Fast codebase search |
| Plan | Inherit | Read-only | Architecture research |
| general-purpose | Inherit | All | Multi-step tasks needing read + write |
context: fork — Where Skills Meet Subagents
The context field controls where a skill runs.
Default (inline) — no context field
The skill loads into your current conversation. Claude executes it right here with full access to everything you've discussed. This is right for reference/convention skills ("use these API patterns") that need to apply to your current work.
context: fork — isolated subagent
The skill content is sent to a brand new Claude instance with no memory of your conversation. The SKILL.md body becomes that subagent's task:
Your conversation: "Summarize this PR" | v Claude spawns a subagent +-----------------------------------------+ | New isolated context: | | [SKILL.md body is all this Claude | | sees — no parent history] | | [does its work independently] | | [returns a summary when done] | +-----------------------------------------+ | v Back in your conversation: "Here's the PR summary: ..."
Fork Is a Confusing Name
The word "fork" suggests copying parent state (like Unix fork() or git fork). But context: fork does the opposite — it creates an isolated, blank context with zero parent history.
When you see context: fork, read it as: "run this in a separate, clean room."
The agent Field
With context: fork, the agent field picks which subagent executes:
--- name: deep-research description: Research a topic thoroughly context: fork agent: Explore --- Research $ARGUMENTS thoroughly: 1. Find relevant files using Glob and Grep 2. Read and analyze the code 3. Summarize findings with specific file references
When to Use context: fork
Good fit:
- Heavy data processing (PR summaries, test analysis, codebase research)
- Pairs perfectly with
\command`` — subagent starts with pre-loaded data - Self-contained tasks that don't need conversation history
Bad fit:
- Reference/convention skills — they need to run inline to apply to current work
- Skills without a concrete task — a forked subagent with only guidelines returns nothing useful
Two Types of Skill Content
Reference content — background knowledge
Runs inline. Conventions, patterns, style guides, domain knowledge.
--- name: api-conventions description: API design patterns for this codebase --- When writing API endpoints: - Use RESTful naming conventions - Return consistent error formats - Include request validation
Task content — step-by-step workflows
Specific actions like deployments. Often paired with disable-model-invocation: true.
--- name: deploy description: Deploy the application to production disable-model-invocation: true --- Deploy the application: 1. Run the test suite 2. Build the application 3. Push to the deployment target
Building Skills Yourself (Outside Claude Code)
Since skills are just tool calls under the hood, you can build the same system yourself with any LLM that supports tool calling.
Step 1: Discover and Parse Skills
import os, yaml def discover_skills(skill_dirs): """Scan directories for SKILL.md files, parse frontmatter.""" skills = {} for base_dir in skill_dirs: for name in os.listdir(base_dir): skill_md = os.path.join(base_dir, name, "SKILL.md") if os.path.exists(skill_md): content = open(skill_md).read() parts = content.split("---", 2) meta = yaml.safe_load(parts[1]) skills[meta.get("name", name)] = { "description": meta.get("description"), "path": skill_md, "body": parts[2] } return skills
Step 2: Register a Skill Tool with Descriptions Injected
skills = discover_skills(["~/.my-agent/skills", "./.my-agent/skills"]) skill_list = "\n".join( f"- {name}: {s['description']}" for name, s in skills.items() ) tools = [{ "type": "function", "function": { "name": "load_skill", "description": f"Load a skill's full instructions.\n\nAvailable skills:\n{skill_list}", "parameters": { "type": "object", "properties": { "skill_name": {"type": "string"} }, "required": ["skill_name"] } } }]
Step 3: Agentic Loop with Skill Loading
import litellm, json, subprocess def run_agent(user_message, model="anthropic/claude-sonnet-4-6", max_turns=20): messages = [{"role": "user", "content": user_message}] for _ in range(max_turns): response = litellm.completion(model=model, messages=messages, tools=tools) message = response.choices[0].message if not message.tool_calls: return message.content messages.append(message) for tool_call in message.tool_calls: name = tool_call.function.name args = json.loads(tool_call.function.arguments) if name == "load_skill": skill = skills[args["skill_name"]] result = skill["body"] elif name == "read_file": result = open(args["path"]).read() elif name == "bash": result = subprocess.run( args["command"], shell=True, capture_output=True, text=True ).stdout messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) return "Max turns reached"
That's the entire architecture. Claude Code adds polish (permissions, subagents, UI, $ARGUMENTS, \command`` preprocessing), but the core is this loop.
Quick Start: Your First Skill
# 1. Create the skill directory mkdir -p ~/.claude/skills/explain-code # 2. Create SKILL.md cat > ~/.claude/skills/explain-code/SKILL.md << 'EOF' --- name: explain-code description: > Explains code with visual diagrams and analogies. Use when explaining how code works, teaching about a codebase, or when the user asks "how does this work?" --- When explaining code, always include: 1. **Start with an analogy**: Compare the code to something everyday 2. **Draw a diagram**: Use ASCII art to show flow or relationships 3. **Walk through the code**: Explain step-by-step what happens 4. **Highlight a gotcha**: What's a common mistake or misconception? Keep explanations conversational. EOF # 3. Test it # In Claude Code, ask "How does this code work?" (auto-trigger) # or type /explain-code src/auth/login.ts (manual trigger)
Practical Tips
Description quality is everything
# Bad description: Helps with code # Good description: > Explains code with visual diagrams and analogies. Use when explaining how code works, teaching about a codebase, or when the user asks "how does this work?"
Skill context budget
Skill descriptions consume context space. The budget is 2% of the context window (fallback: 16,000 characters). If you have many skills, some may be excluded. Check /context for warnings. Override with the SLASH_COMMAND_TOOL_CHAR_BUDGET environment variable.
Keep SKILL.md under 500 lines
Move detailed reference material to separate files and link them:
## Additional resources - For complete API details, see [reference.md](reference.md) - For usage examples, see [examples.md](examples.md)
Claude reads these only when needed.
Permissions
- Deny all skills: add
Skillto deny rules in/permissions - Allow specific:
Skill(commit),Skill(review-pr *) - Deny specific:
Skill(deploy *)
Summary
| Feature | Implementation |
|---|---|
| Lazy loading | Three tiers: descriptions → body → supporting files |
| Trigger mechanism | Plain tool calls with Skill(name) |
| Scope control | Enterprise > personal > project > plugin |
| Dynamic data | \command`` preprocessor injects shell output |
| Isolation | context: fork runs skill in a subagent |
| Cross-tool portability | Agent Skills open standard (20+ tools) |
Key Takeaways
- Skills are knowledge, not actions: MCP gives Claude hands, skills give it a handbook
- Lazy loading keeps context cheap: Only descriptions are always loaded
- It's just tool calls: No magic — a
Skilltool whose description is a dynamic menu - Description quality decides everything: Claude can only trigger what it can identify
context: forkisolates heavy work: Pair with\command`` for zero-roundtrip starts- You can build this yourself: ~50 lines of Python with any tool-calling LLM
When to Reach for Skills
Good fit:
- Encoding team conventions (API patterns, commit style)
- Automating repeatable workflows (issue fixes, deploys, reviews)
- Injecting domain knowledge Claude needs occasionally
- Standardizing how Claude handles specific file types or tasks
Bad fit:
- One-off instructions (just say them in chat)
- New actions that require code (use MCP tools)
- Things that belong in CLAUDE.md (always-on project context)
Next Steps
- Create your first skill in
~/.claude/skills/— start with a reference skill for your coding conventions - Try
context: forkwith\command`` for a PR summary skill - Experiment with
disable-model-invocation: truefor destructive commands - Read the Agent Skills open standard if you want cross-tool portability
The fastest way to understand skills is to write one and watch Claude use it. Start small — even a 20-line SKILL.md that captures "here's how we name things" can transform how Claude collaborates with you.
Good luck building your handbook!
