Blogs

Claude Skills: The Complete Guide to Teaching Claude New Tricks

2026-04-1725 min read

Skills turn Claude into a specialist on demand. Learn how the three-tier lazy loading system works, how skills differ from MCP tools, and how to build your own skills from scratch.

Claude Skills: The Complete Guide to Teaching Claude New Tricks

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

  1. The core idea: Skills give Claude knowledge and playbooks, not new actions
  2. The three-tier loading system: How skills stay cheap on context until they're needed
  3. The secret: Skills are just regular tool calls dressed up in markdown
  4. Subagents and context: fork: How skills can run in isolated mini-conversations
  5. 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

TierWhat 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

FieldRequired?Description
nameNoBecomes the /slash-command. Defaults to directory name.
descriptionRecommendedClaude uses this to decide when to auto-load it.
disable-model-invocationNotrue = only the user can trigger. Default: false.
user-invocableNofalse = hidden from / menu. Default: true.
allowed-toolsNoTools Claude can use without asking permission.
contextNofork = run in an isolated subagent.
agentNoWhich subagent type handles context: fork.
modelNoModel override when skill is active.
argument-hintNoAutocomplete 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:

SettingUser invokes?Claude invokes?Use case
DefaultsYesYesGeneral-purpose skills
disable-model-invocation: trueYesNoDangerous actions: /deploy, /send-email
user-invocable: falseNoYesBackground 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:

LocationPathApplies to
EnterpriseManaged settingsAll users in your org
Personal~/.claude/skills/<name>/SKILL.mdAll your projects
Project.claude/skills/<name>/SKILL.mdThis project only
Plugin<plugin>/skills/<name>/SKILL.mdWhere 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:

  1. Scans the SKILL.md body for any \...`` patterns
  2. Executes each as a shell command on your machine
  3. Replaces the pattern with the command's stdout
  4. 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:

  1. Start the skill
  2. Call Bash to run gh pr diff → wait
  3. Call Bash to run gh pr view --comments → wait
  4. Call Bash to run gh pr diff --name-only → wait
  5. 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

SubagentModelToolsPurpose
ExploreHaikuRead-onlyFast codebase search
PlanInheritRead-onlyArchitecture research
general-purposeInheritAllMulti-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 Skill to deny rules in /permissions
  • Allow specific: Skill(commit), Skill(review-pr *)
  • Deny specific: Skill(deploy *)

Summary

FeatureImplementation
Lazy loadingThree tiers: descriptions → body → supporting files
Trigger mechanismPlain tool calls with Skill(name)
Scope controlEnterprise > personal > project > plugin
Dynamic data\command`` preprocessor injects shell output
Isolationcontext: fork runs skill in a subagent
Cross-tool portabilityAgent Skills open standard (20+ tools)

Key Takeaways

  1. Skills are knowledge, not actions: MCP gives Claude hands, skills give it a handbook
  2. Lazy loading keeps context cheap: Only descriptions are always loaded
  3. It's just tool calls: No magic — a Skill tool whose description is a dynamic menu
  4. Description quality decides everything: Claude can only trigger what it can identify
  5. context: fork isolates heavy work: Pair with \command`` for zero-roundtrip starts
  6. 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

  1. Create your first skill in ~/.claude/skills/ — start with a reference skill for your coding conventions
  2. Try context: fork with \command`` for a PR summary skill
  3. Experiment with disable-model-invocation: true for destructive commands
  4. 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!