Claude Code Skills vs Subagents vs Plugins vs Hooks (2026)
Claude Code has four extension primitives — Skills, Subagents, Plugins, and Hooks — and most teams reach for one when they should reach for another. This is the field guide we wish we’d had on day one: what each primitive actually is, the real config that ships one, the decision tree that tells you which to reach for, and the footguns we hit so you don’t have to.

On this page · 12 sections▾
TL;DR + decision tree
- If you want Claude to learn a procedure — a workflow, a coding convention, a checklist that should kick in when relevant — write a Skill. A folder under
.claude/skills/<name>/with aSKILL.mdfile gets auto-loaded when the conversation matches the frontmatter description. - If you want to isolate exploratory or noisy work from your main context — code review, deep file scans, research passes — spawn a Subagent. A Markdown file under
.claude/agents/defines a child Claude with its own system prompt, tool allowlist, and context window. - If you want to distribute your team’s tooling as one install — a bundle of slash commands, hooks, skills, and MCP-server configs — ship a Plugin. Teammates run
/plugin installonce and the whole bundle shows up. - If you want to enforce a policy deterministically — block
rm -rf, inject context at session start, log every file edit, require a confirmation on writes to prod — write a Hook. A registration insettings.jsonruns a bash command at a lifecycle event and can block or modify what Claude does next.
These four are not mutually exclusive and they are not substitutes for each other. They live at different layers of the same agent runtime, and almost every mature Claude Code setup uses at least three of the four. The rest of this post is a tour of each, with real config you can paste in, then the patterns we’ve seen for composing them.
How Claude Code’s extension surface works
Before going primitive by primitive, it helps to picture the runtime as a stack with four orthogonal slots, each answering a different question.
Skills answer “what does the model know to do?” They sit on disk as Markdown procedures with YAML frontmatter. At runtime the model scans the descriptions of every available Skill — local under .claude/skills/, user-wide under ~/.claude/skills/, or shipped by an installed Plugin — and loads the body of whichever Skill the current turn looks like it needs. The trigger is content matching: if the conversation is about generating a PDF and a Skill exists named pdf-generation with a description that mentions PDFs, that Skill’s body lands in context.
Subagents answer “who is doing the work right now?” The main conversation can spawn a subagent via the Task tool, hand it a goal, and wait for a summary. The subagent runs with its own fresh context window, its own system prompt, its own tool allowlist, and (optionally) a different underlying model. When it finishes, only the summary returns to the parent. The parent never sees the logs, file dumps, or scratch reasoning the subagent generated. This is how Claude Code reads a 10,000-line codebase without poisoning the main context with file contents you’ll never reference again.
Plugins answer “how does this tooling distribute?” They are bundles — a manifest plus a directory of slash commands, hooks, subagents, skills, and MCP-server config — installed via the /plugin command from a marketplace, a Git URL, or a local path. A Plugin is the answer to “how do I get the whole team on the same setup in one command.” From the model’s perspective at runtime, a Plugin’s Skill is indistinguishable from a hand-written local Skill; the packaging happens at install time.
Hooks answer “what must happen deterministically around the agent loop?” They are not invoked by the model and the model cannot decline to fire them. Registered in settings.json under a top-level hooks key, each hook binds to a lifecycle event (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SubagentStop, PreCompact, and roughly two dozen others) and runs a bash command, an HTTP call, a prompt, an agent, or even an MCP tool when that event fires. Hooks are how you enforce policy, audit-log everything, inject context that should always be present, and redirect or block the model without trusting it to follow instructions.
The reason this matters: each primitive is opinionated about a different question. If you reach for the wrong one, the result feels brittle. Putting a coding convention in a Hook’s SessionStart injection looks clever for a week, then breaks when the convention needs to be conditional on the file being edited — a Skill would have handled that natively. Putting a destructive-command block in a Skill is theatre — the model will load the Skill, acknowledge the instruction, and then ignore it three turns later when it’s about to write tests. Hooks are for things the model cannot be trusted to follow; Skills are for things the model needs to know.
These primitives also evolve at different speeds. Skills and Plugins are stable, well-documented, and increasingly portable across agents (Cursor, Codex, Cline, and Gemini CLI all parse SKILL.md). Subagents and Hooks are more Claude-Code-specific and pick up new events and capabilities with most Claude Code releases. If portability across editors matters to you, lean on Skills first and treat the rest as Claude-Code-local glue. We covered the cross-agent portability story at length in cross-agent skills portability.
Side-by-side matrix
Five dimensions, four primitives. Every cell comes from the official Claude Code docs as of May 2026; corner cases and edge behaviors live in the per-primitive sections below.
| Dimension | Skills | Subagents | Plugins | Hooks |
|---|---|---|---|---|
| Lives in | .claude/skills/<name>/SKILL.md | .claude/agents/<name>.md | ~/.claude/plugins/<name>/ + /plugin marketplace | .claude/settings.json hooks key |
| Activates by | Frontmatter description matches conversation | Explicit invocation via Task tool | User runs /plugin install | Lifecycle event fires (PreToolUse, etc.) |
| Scope | Procedural knowledge (workflows, conventions) | Isolated context window for a side task | Distribution bundle of other primitives | Deterministic policy / observability |
| Composability | Loaded into main or subagent context | Can use any tool + load Skills | Ships Skills, Subagents, Hooks, MCPs together | Runs around any tool, including subagent tools |
| Best for | Teaching a procedure that should auto-activate | Parallelizing or isolating noisy work | Shipping your team's stack as one install | Enforcing policy the model cannot opt out of |
| Trust boundary | Trusted code path (runs in agent context) | Sandboxed context, inherits tool allowlist | Trusted on install, audit before adopting | Runs as the user, full filesystem access |
| Cross-agent portability | Strong (agentskills spec, Cursor/Codex/Cline/Gemini) | Weak (Claude Code-specific) | Weak (Claude Code-specific) | Weak (Claude Code-specific) |
Three takeaways. First, only Hooks fire deterministically — every other primitive depends on the model deciding to use it. Second, only Plugins distribute; if your team needs the same setup, Plugins are the carrier and everything else is cargo. Third, Skills are the most portable across agents because they conform to the open agentskills spec — write a Skill once and Cursor, Codex, Cline, and Gemini CLI can read it.
Skills — Markdown procedures that auto-activate
A Skill is a folder containing a SKILL.md file with YAML frontmatter and a Markdown body. The frontmatter tells Claude when this Skill should activate; the body tells Claude what to do. The minimal shape:
# .claude/skills/pdf-generation/SKILL.md
---
name: pdf-generation
description: Generate brand-styled PDF reports from Markdown using the company's standard cover page, header, footer, and the Inter typeface. Use whenever the user asks for a PDF, a printable report, or a brand-styled document.
model: sonnet
---
# PDF Generation
## When this applies
The user wants a PDF that follows the company brand guidelines. This
includes:
- Quarterly business reviews
- Customer-facing reports
- Internal one-pagers exported for sharing
## Procedure
1. Read the source Markdown the user supplied (or generate one if
they describe the content).
2. Confirm the cover-page metadata: title, date, author, audience.
3. Apply the template at templates/brand.tex.j2 — never invent a new one.
4. Render via 'uv run scripts/render-pdf.py <input.md> <output.pdf>'.
5. Verify the output opens and the cover page renders. Report the file
path back to the user.
## Helper files
- templates/brand.tex.j2 — LaTeX template, do not edit without design
approval
- scripts/render-pdf.py — render script with the right defaults
## What not to do
- Do not hand-roll new templates. Use the supplied template.
- Do not include the company logo at >120px height in body content.
- Do not export password-protected PDFs unless the user asks.What it does best
Skills excel at procedures that should activate without the user remembering to mention them. The frontmatter description is the activation contract: Claude scans descriptions of all available Skills every turn and loads the body of whichever matches the current conversation. That makes Skills the right home for “how we do X here” knowledge — coding conventions, report templates, review checklists, deployment runbooks — that you want applied consistently across sessions without the user pasting context every time.
Reach for it when…
- A procedure is repeating across sessions and you’re pasting the same instructions every time
- The instructions are long enough (200+ words) that putting them in CLAUDE.md would bloat every turn unnecessarily
- The procedure is conditional — it should kick in for some tasks but stay out of the way otherwise
- You want to share the procedure across editors (Cursor, Codex, Cline, and Gemini all parse SKILL.md)
How to ship one
Three files in a folder, no install step. Drop them at .claude/skills/<name>/ for project scope or ~/.claude/skills/<name>/ for user-wide scope. The minimum is the SKILL.md above; helper scripts, templates, and data files live alongside it and the Skill body references them by relative path.
# Bash session showing a minimal Skill scaffold
mkdir -p .claude/skills/code-review
cat > .claude/skills/code-review/SKILL.md <<'EOF'
---
name: code-review
description: Run the team's code-review checklist on a diff. Use whenever the user asks for a code review, a PR review, a "before I ship" review, or wants you to look at uncommitted changes.
---
# Code Review
## Checklist
For each changed file in the diff:
1. SQL safety: parameterised queries, no string-formatted SQL.
2. LLM trust boundary: any value sourced from a model output is treated
as untrusted user input.
3. Conditional side effects: side effects only in code paths the user
explicitly triggers.
4. Error handling: errors surface with context, not silent fallbacks.
5. Tests: behavior-change PRs have at least one test, even if smoke.
Produce a structured review: per-file findings, then a summary with a
ship / hold recommendation.
EOF
git add .claude/skills/code-review
git commit -m "Add code-review skill"That’s it. Next time someone in your team says “review this PR” in Claude Code, the description matches, the Skill loads, and the checklist is applied. No client install step, no marketplace, no token cost when the Skill is not loaded.
Skip it when…
- The behavior needs to be enforced, not just suggested — the model will sometimes ignore Skill instructions and that’s a Hook’s job
- The procedure depends on live external state (e.g. fetching from an API) — that’s an MCP server’s job
- The procedure is two sentences long — put it in CLAUDE.md instead, the always-loaded cost is negligible
Subagents — child Claude instances with fresh context
A Subagent is defined as a Markdown file under .claude/agents/<name>.md with frontmatter describing its name, the tools it’s allowed to call, and (optionally) the model to use. The body is the system prompt. When the main conversation invokes the Task tool pointing at this Subagent, Claude Code spawns a fresh agent with its own context window, runs the goal, and returns only the final response to the parent.
# .claude/agents/codebase-explorer.md
---
name: codebase-explorer
description: Read-only deep exploration of a codebase. Spawn this when the question is "where is X defined" or "how does Y work end-to-end" and the answer requires reading many files.
tools: Read, Grep, Glob
model: sonnet
---
You are a codebase exploration specialist. Your job is to answer
"where is X" and "how does Y work" questions by reading the codebase
exhaustively and returning a concise structured summary.
## Operating rules
- You may read any file. You may not write or edit any file.
- Prefer Grep to find symbols. Prefer Glob to find related files.
- Read the smallest set of files needed to answer the question
confidently. Do not read entire directories.
- Return a structured report:
- **Where it lives**: file paths with line numbers
- **How it works**: 3-5 sentence narrative
- **What depends on it**: callers, importers, related modules
- **Gotchas**: anything the calling agent should know
Do not editorialize. Do not propose changes. Your job is to look,
not to act.What it does best
Subagents excel at side tasks that would otherwise pollute the main conversation with logs, file contents, or scratch reasoning the parent will never reference again. The parent says “use the codebase-explorer agent to find where X is defined,” waits, and receives a four-bullet summary. The 50 file reads the explorer did to produce that summary never enter the parent’s context. Combine with a tight tool allowlist and you have a primitive that’s great for safe parallelism: the parent can spawn three Subagents at once, each exploring a different region of the codebase, and merge their outputs.
Reach for it when…
- A side task will read a lot of files or generate a lot of tool output that the parent doesn’t need to retain
- You want to parallelize work — three Subagents working in parallel finish faster than one main loop doing the same work serially
- You want a different tool allowlist for a sub-task than for the main conversation (e.g. read-only for exploration, write-allowed for the main loop)
- You want a different system prompt for a specialized job — a code-review subagent, a test-writing subagent, a research-summarizer subagent
How to ship one
One Markdown file under .claude/agents/, no further config. The parent invokes it by name through the Task tool. In practice you describe the Subagent in plain English in your prompt — “use the codebase-explorer agent to find where the auth boundary is defined” — and Claude Code dispatches the Task tool with the matching agent.
# How a Subagent gets invoked from a session
# 1. The user (or the main agent) decides to delegate.
# 2. Claude Code calls the Task tool with parameters:
{
"subagent_type": "codebase-explorer",
"description": "Find where the auth boundary is defined",
"prompt": "Find every file that touches the auth boundary in this
repo. I need: where the boundary is defined, what files
call into it, what tests cover it, and any pre-existing
comments about its security posture. Return a structured
report following your operating rules."
}
# 3. A fresh Claude instance spawns with:
# - System prompt: contents of codebase-explorer.md (below frontmatter)
# - Tool allowlist: Read, Grep, Glob (from frontmatter)
# - Model: sonnet (from frontmatter)
# - Empty context window — does NOT see the parent's history
# - The prompt from step 2 as its first user turn
#
# 4. The Subagent runs an agentic loop. It might do 30 tool calls.
# None of them appear in the parent's context.
#
# 5. The Subagent's final assistant message is returned to the parent
# as the Task tool's result. The parent sees exactly one message.Skip it when…
- The task is short enough that spawning a fresh context costs more than the noise it would have generated
- The parent will need to follow up with questions that depend on the Subagent’s intermediate state — the parent only ever sees the final summary
- You need deterministic behavior — Subagents are still agents, with the same model-decides-what-to-do dynamics as the parent
Plugins — bundled distribution for everything else
A Plugin is the distribution layer for Claude Code. One package can ship slash commands, hooks, subagents, skills, and MCP-server configs as a single install. Teammates run /plugin install once and the whole bundle merges into their setup. Plugin assets live under ~/.claude/plugins/<name>/ and reference each other with $CLAUDE_PLUGIN_DIR, so they don’t pollute the project’s .claude/ folder.
# ~/.claude/plugins/team-stack/plugin.json
{
"name": "team-stack",
"version": "1.4.2",
"description": "Our team's standard Claude Code stack: skills for
code review and ADR writing, a deploy subagent, a
PreToolUse hook blocking destructive Bash, and the
Linear MCP server preconfigured.",
"author": "Internal Tools",
"license": "Apache-2.0",
"components": {
"skills": ["skills/code-review", "skills/adr-author"],
"agents": ["agents/deploy-shepherd.md"],
"hooks": "hooks/hooks.json",
"mcpServers": "mcp/servers.json",
"commands": ["commands/ship.md", "commands/rollback.md"]
}
}
# Directory tree:
# ~/.claude/plugins/team-stack/
# +- plugin.json
# +- skills/
# | +- code-review/SKILL.md
# | +- adr-author/SKILL.md
# +- agents/
# | +- deploy-shepherd.md
# +- hooks/
# | +- hooks.json
# | +- block-rm.sh
# +- mcp/
# | +- servers.json
# +- commands/
# +- ship.md
# +- rollback.mdWhat it does best
Plugins are how you ship the rest of the stack. Skills, Subagents, Hooks, and MCP-server configs are all distributable individually, but a Plugin bundles them into one versioned package with a manifest. The big win is onboarding: a new teammate installs the team Plugin and they have your slash commands, your hooks, your skills, and your MCP servers all wired up in one command. The second win is versioning: Plugins have a version field in the manifest, so you can ship a v1.4.2 and roll back if a hook causes problems.
Reach for it when…
- You have more than two people working in the same Claude Code environment and you want them on the same setup
- You’ve built a stack worth open-sourcing or sharing publicly — Plugins are the canonical share unit
- You want versioned, rollback-able tooling rather than ad-hoc copy-pasted
.claude/directories - You want to consume someone else’s carefully-tuned stack rather than rebuild from scratch
How to ship one
Author the bundle locally, push to a Git repo, install via the /plugin marketplace add + /plugin install flow. The plugin manifest declares which Skills, Agents, Hooks, MCP servers, and commands are in the bundle; Claude Code reads the manifest and wires them in.
# Author flow (your machine)
mkdir -p team-stack/{skills/code-review,agents,hooks,mcp,commands}
# Create plugin.json (manifest), then add components.
$EDITOR team-stack/plugin.json
$EDITOR team-stack/skills/code-review/SKILL.md
$EDITOR team-stack/agents/deploy-shepherd.md
$EDITOR team-stack/hooks/hooks.json
$EDITOR team-stack/hooks/block-rm.sh
chmod +x team-stack/hooks/block-rm.sh
$EDITOR team-stack/mcp/servers.json
git init team-stack
cd team-stack
git add .
git commit -m "team-stack 1.0.0"
git remote add origin [email protected]:acme/team-stack.git
git push -u origin main
# Teammate flow (their machine)
# 1. Add the marketplace (one-time)
/plugin marketplace add github:acme/team-stack
# 2. Install the plugin
/plugin install team-stack
# 3. Confirm
/plugin list
# -> team-stack 1.0.0 (installed)
# skills: code-review, adr-author
# agents: deploy-shepherd
# hooks: PreToolUse(Bash, rm -rf)
# mcp: linear
# commands: /ship, /rollbackSkip it when…
- You’re the only person using your
.claude/directory — local files are simpler than a Plugin manifest - Your setup is changing every day — Plugins are a versioned contract, not a scratchpad
- You need cross-editor portability — a Plugin only installs in Claude Code; Skills inside it are portable but the packaging is not
Hooks — deterministic event handlers
A Hook is a bash command (or HTTP call, or prompt, or agent dispatch) registered in settings.json under a top-level hooks key. Each hook binds to a lifecycle event — PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SubagentStop, PreCompact, and roughly two dozen others — and runs deterministically when that event fires. Critically, the model cannot opt out: hooks fire as part of the runtime, not as part of an agent decision. That makes them the right primitive for policy enforcement, audit logging, and always-on context injection.
# .claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/audit-log.sh",
"timeout": 10
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/inject-context.sh"
}
]
}
]
}
}
# .claude/hooks/block-rm.sh
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE 'rm[[:space:]]+-rf|rm[[:space:]]+-fr'; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "rm -rf blocked by .claude/hooks/block-rm.sh — use git clean or specify files explicitly"
}
}'
else
exit 0
fiWhat it does best
Hooks excel at the “trust but verify” layer around the agent loop. The model is good at intent and bad at consistency; Hooks fill the consistency gap. A PreToolUse hook on Bash that blocks rm -rf cannot be argued with — the model can write the command, the runtime calls the hook, the hook returns deny, the command never runs. The same primitive supports observability: a PostToolUse hook on Edit and Write writes a line to an audit log every time the model edits a file. And it supports always-on context injection: a SessionStart hook can fetch the current Jira ticket and add it to the context before the user’s first turn.
Reach for it when…
- You need a policy enforced regardless of what the model decides — blocking destructive commands, requiring approval on prod writes, denying network calls to unapproved hosts
- You need an audit trail that’s independent of the model’s output — every file edit, every shell command, every MCP call, logged to disk or a remote collector
- You need context injected reliably at the start of every session — current Git branch, current ticket, current on-call rotation
- You need to redirect or rewrite something the model is about to do — e.g. inject a sandbox prefix on every Bash command targeting a specific directory
How to ship one
Register in settings.json, point at a script, make the script executable. The hook block can declare an if matcher condition (so the script only fires on the calls you care about), a timeout in seconds, and a type (command, prompt, agent, http, or mcp). The script reads the tool-call payload from stdin as JSON and either exits 0 (allow), exits 2 (block with stderr as the message), or exits 0 with a structured JSON payload on stdout.
# Per the Claude Code docs, a hook handler can block in two ways.
#
# (1) Exit code 2 — stderr becomes the user-facing reason.
# This is the quick way; useful for one-liner scripts.
#
# (2) Exit code 0 with structured JSON on stdout — Claude reads the
# permissionDecisionReason and adjusts its plan. This is the
# preferred way for anything more than a quick "no".
# Quick-block example (exit 2):
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$COMMAND" == *"DROP TABLE"* ]]; then
echo "Blocked: DROP TABLE detected" >&2
exit 2
fi
exit 0
# Structured-block example (exit 0 + JSON):
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command' < /dev/stdin)
if [[ "$COMMAND" == *"DROP TABLE"* ]]; then
jq -n '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "DROP TABLE is blocked outside migrations. Use a numbered migration file under db/migrations/ and run via the migrate command."
}
}'
exit 0
fi
exit 0Skip it when…
- The behavior is “nice to have” rather than “must enforce” — Skills are better for nudges, Hooks are for hard rules
- The check is slow (more than ~100ms) and would fire on every tool call — that’s a latency tax the model pays on every turn
- The decision genuinely depends on conversational context the hook can’t see — hooks get the tool payload, not the conversation history
Composition patterns
These four primitives are most powerful when stacked. After running them in production we keep returning to the same handful of compositions; if you’re early in your Claude Code journey, copy these shapes before inventing your own.
Pattern 1: Plugin ships a Skill + a Hook together
A Plugin called brand-pdf ships a pdf-generation Skill (procedural knowledge for generating the company-branded PDF) plus a PostToolUse hook that runs after every PDF render to log the file path and size to brand-pdf.log. The Skill is the teaching layer (“here’s how to make a PDF”); the Hook is the observability layer (“every PDF we generated this quarter, listed”). Bundling them in one Plugin means a teammate installs once and gets both behaviors. If we shipped them separately, two of three teammates would install only one and the observability data would be incomplete.
Pattern 2: Subagent loads a Skill for its specialty
A code-reviewer Subagent under .claude/agents/ has a tight system prompt (“you review code”) and a tool allowlist (Read, Grep, Glob). When the parent invokes it, the Subagent loads the code-review Skill from .claude/skills/code-review/ because the Skill description matches the conversation. The Subagent then applies the Skill’s 5-point checklist file by file and returns a structured report to the parent. The parent never sees the file reads, only the report. The same Skill also works when called from the main conversation; the Subagent just gives it a clean room to operate in.
Pattern 3: SessionStart hook seeds context for Skills
A SessionStart hook fetches the current Git branch, the current Jira ticket, and the on-call engineer’s name, and injects them into the context as a system message. When later in the session a Skill activates (say a deploy-runbook Skill), it has access to that context and can reference “the current ticket” without the model needing to call a tool to find out which ticket. The Hook handles the deterministic environment fetch; the Skill handles the workflow. Together they feel seamless. Separately they require manual prompting every session.
Pattern 4: PreToolUse hook hardens a Subagent
Subagents inherit the tool allowlist from their frontmatter, but they also inherit the parent’s Hook configuration. That means a PreToolUse hook in settings.json that blocks destructive Bash applies to every Subagent the main agent spawns, automatically. So you can spawn a research Subagent and trust that it can’t accidentally wipe a directory, even if its system prompt is short and its safety reasoning is thin. The Hook is your last line of defense, and it covers the whole agent tree by default.
Pattern 5: Plugin distributes a curated MCP set + Hook
A Plugin called linear-stack ships the Linear MCP server config plus a UserPromptSubmit hook that scans the user’s prompt for Linear issue keys (e.g. ENG-1234) and pre-fetches those issues via the Linear MCP, injecting the fetched issue body into context. The Hook does the deterministic fetching; the MCP server does the authenticated network work. The model never has to remember “oh I should look up that ticket” — the ticket arrives in context the same turn the user mentioned it. This pattern generalizes: any MCP server that returns rich context per identifier is a candidate for a UserPromptSubmit hook that pre-fetches the identifiers it sees in the prompt.
Pattern 6: Skill explains the slash command a Plugin ships
Plugins can ship slash commands as Markdown files. A command like /ship might be a 200-line script. Documenting it inside the command Markdown is fine for the quick view; documenting it as a Skill (ship-command with a description like “the user is using the /ship command or asking how it works”) means the model can explain the command, debug it, and adapt it when something breaks. The Plugin ships the command; the Skill ships the knowledge about the command. Both live in the same Plugin package.
Decision flowchart
Five questions, four primitives. Run the questions top-down; stop at the first “yes” and you’ll reach for the right primitive in nearly every case.
1. Must this happen even if the model decides not to?
Yes → Hook. Hooks fire on lifecycle events deterministically; the model cannot decline. Policy enforcement, audit logging, always-on context injection all live here.
2. Will this side task generate output the parent doesn’t need to retain?
Yes → Subagent. Spawn a fresh context window, let the child do the noisy work, get back only the summary. Code review passes, deep file scans, research summaries all benefit.
3. Should this behavior teach the model a workflow that activates when relevant?
Yes → Skill. SKILL.md with a description that the model matches against the conversation. Coding conventions, report templates, review checklists, runbooks all live here.
4. Does this stack need to be installed by more than one person, with versioning and rollback?
Yes → Plugin. Bundle the Skills, Subagents, Hooks, and MCP-server configs into one package, ship via a marketplace or Git URL, install via /plugin install.
5. None of the above?
Then it’s probably a one-liner in CLAUDE.md, or an MCP server (if the task needs authenticated access to a live external system). For an MCP-vs-Skill decision matrix, see our Skills vs MCP vs Subagents vs CLI deep dive.
The reason this flowchart works in that order: enforcement is the strongest constraint (a Hook can’t be opted out of), isolation is the next strongest (a Subagent guarantees a clean context), teaching is third (Skills rely on the model recognizing relevance), and distribution is orthogonal to all three. If you skip the order and reach for a Skill where you needed a Hook, you’ll rebuild it as a Hook within a week.
Pitfalls we’ve seen in production
Skills with vague descriptions never activate
The model decides whether to load a Skill by comparing the conversation to the Skill’s frontmatter description. A description like “Useful for code-related tasks” matches everything and nothing, so the model picks based on other signals and your Skill gets ignored. Write the description as if explaining to a new hire when to use this skill: list the symptoms (“user asks for a PDF, a printable, a branded report”), not the capability. A 60-word activation contract beats a 10-word one every time.
PreToolUse hooks that error lock you out
If a PreToolUse hook script crashes (typo, missing jq, wrong permissions on the file), it can fail in ways that block the matched tool entirely. A hook that’s supposed to block rm -rf but instead blocks all Bash is painful to debug because the model can’t run any investigation commands. Always test hooks in .claude/settings.local.json first — that file is gitignored, so a broken hook only affects your own session. Test with chmod +x, test with a deliberately triggering input, test with a deliberately non-triggering input. Only then promote to settings.json and commit.
Subagent tool allowlist forgotten
If you omit the tools field from a Subagent’s frontmatter, it inherits a default allowlist that’s often broader than you intended — including Bash and write tools. Subagents named for read-only work (codebase-explorer, research-summarizer) should explicitly list Read, Grep, Glob and nothing else. The safety property of “a Subagent that cannot write” is something you have to declare.
Plugins shipping untested Hooks
Hooks inside a Plugin run on every teammate’s machine with their filesystem permissions. A hook that works on the author’s mac (Bash 5, GNU jq) may misbehave on a teammate’s Linux box or an older shell. Treat Plugin Hooks like production scripts: test on at least two environments before publishing the Plugin, pin tool versions where you can, and bump the Plugin version when the Hook semantics change. The Plugin manifest’s version field is your rollback lever.
Skills competing for activation
Once you have more than 5-6 Skills installed, the descriptions start fighting for the model’s attention. Two Skills with similar descriptions (code-review and pr-review, say) can produce inconsistent behavior: sometimes one activates, sometimes the other, sometimes both. Pick a canonical name per task and delete the duplicates. Also remember: every Skill description (not body) is scanned each turn, so 20 wordy descriptions cost real tokens. Keep descriptions tight.
Hook timeouts hiding real failures
A hook with no timeout field can hang indefinitely if its script never exits — and the agent waits. Worse, when you do set a timeout, a hook that silently times out doesn’t emit a deny decision and the tool runs anyway. Always set a tight timeout (5-10s is generous for most checks) and exit cleanly on timeout in the script itself; don’t rely on the runtime to clean up. If you’re running anything slow in a hook (e.g. network call), move it async and write to disk rather than blocking the agent loop.
Treating a Plugin as a private mutable store
Plugins live under ~/.claude/plugins/ and look like local files, so people sometimes edit them directly on their machine — tweaking a Skill body or a Hook script. The next /plugin update overwrites those edits. If you want to customize a Plugin’s behavior locally, override it: create a local Skill with the same name as the Plugin’s Skill (local files win), or add a local hook that runs after the Plugin’s. Never edit Plugin files in place.
Frequently asked questions
What's the difference between Skills, Subagents, Plugins, and Hooks in Claude Code?
Skills are Markdown procedures (SKILL.md with YAML frontmatter) that Claude loads when the conversation matches their description. Subagents are spawned child Claude instances with their own context window, system prompt, and tool allowlist — defined as Markdown under .claude/agents/. Plugins are distributable bundles installed via the /plugin command that can ship Skills, Subagents, Hooks, MCP servers, and slash commands as one unit. Hooks are deterministic event handlers registered in settings.json that run bash commands at lifecycle events like PreToolUse or UserPromptSubmit, and can block or modify what Claude does. Skills teach, Subagents isolate, Plugins distribute, Hooks enforce.
Can Skills, Subagents, Plugins, and Hooks be used together?
Yes — they compose. A single Plugin can ship two Skills, one Subagent, and a Hook in the same package. A Subagent can load a Skill mid-task. A Hook can run at SubagentStop to write the result back to disk. A Skill can document the exact slash command a Plugin installs. The four primitives are deliberately orthogonal: Skills carry instructions, Subagents carry isolated context, Plugins carry distribution, Hooks carry deterministic enforcement. Most production Claude Code setups end up running at least three of the four after a few months.
When should I write a Skill instead of a Subagent?
Write a Skill when the procedure is reusable knowledge: a coding convention, a workflow, a template, a checklist. The main conversation loads it on demand and applies it inline. Spawn a Subagent when the side task would otherwise pollute the main context with logs, search results, or file scans you'll never reference again. A Subagent gets a fresh context window, does the work, and returns only a summary. Many setups combine both: a Subagent for a code-review pass, loading a Skill that defines the review checklist.
Where do hooks live in Claude Code settings.json?
Hooks are configured under a top-level hooks key in settings.json, scoped at three levels: ~/.claude/settings.json (user-wide), .claude/settings.json (project, shareable via git), and .claude/settings.local.json (project, gitignored). Plugins can also ship hooks via ~/.claude/plugins/<name>/hooks/hooks.json. Each hook event (PreToolUse, PostToolUse, UserPromptSubmit, Stop, SessionStart, SubagentStop, PreCompact, and a couple of dozen more) maps to an array of matcher groups; each matcher group runs an array of hook handlers (command, prompt, agent, MCP tool, or HTTP). Three levels of nesting: event → matcher → handler.
How does a hook block a dangerous tool call?
Two mechanisms. Exit code 2 from a PreToolUse hook script blocks the tool call outright; whatever the script wrote to stderr becomes the message Claude sees. The structured alternative is to exit 0 with a JSON payload on stdout containing hookSpecificOutput.permissionDecision set to deny (or allow, ask, defer) plus permissionDecisionReason. The structured form is more robust because Claude can read the reason and adjust its plan. A canonical example: a PreToolUse hook matching Bash that pattern-matches rm -rf and returns deny — your repo is now safe from agent-induced data loss without disabling Bash entirely.
Are Plugins just a packaging format, or do they have runtime semantics?
Mostly packaging, with one runtime wrinkle. A Plugin is a bundle of slash commands, hooks, subagents, skills, and MCP-server config installed via the /plugin command from a marketplace or a Git URL. Once installed, its slash commands and hooks merge into your settings; its subagents and skills appear alongside locally-defined ones; its MCP servers connect like any other. The runtime wrinkle is that Plugin assets live under ~/.claude/plugins/<name>/ and reference each other with $CLAUDE_PLUGIN_DIR, which keeps your project's .claude/ folder clean. From the model's perspective, an installed Plugin's Skill is indistinguishable from a local Skill.
Will writing many hooks slow Claude Code down?
Yes if you let them. Every PreToolUse hook runs before every matched tool call, synchronously, and the agent waits for the exit code. A 200ms hook on a tool that fires 30 times per turn adds 6 seconds of wall time the model spends idle. The defaults that ship safe are: scope matchers tightly (matcher: Bash with an if: Bash(rm *) condition runs only on rm calls, not all Bash calls); set a timeout in the hook block (the docs accept a timeout field); push expensive analysis to PostToolUseFailure or async events where blocking is not the goal. Audit your hook timings after a week — most teams discover one hook is responsible for half the slowdown.
Do Skills, Subagents, Plugins, and Hooks work outside Claude Code?
Mixed. Skills are the most portable — they are Markdown with YAML frontmatter, and vercel-labs/skills, mastra-ai/skills, and google/skills all conform to the same SKILL.md format that Claude Code parses. So a Skill written for Claude Code often loads in Cursor, Codex, Cline, or any agent that implements the agentskills spec. Subagents, Plugins, and Hooks are more Claude-Code-specific. Cursor has its own rules system, OpenAI Codex has its own delegation primitive, and most editors don't yet expose hook lifecycles. If portability is a hard requirement, prefer Skills as the carrier and keep Plugin / Hook / Subagent logic Claude-Code-local.
Can a Subagent invoke a Hook?
Indirectly, via the events the subagent itself triggers. When a Subagent runs, it fires SubagentStart and SubagentStop hook events at the orchestration layer; its own tool calls fire PreToolUse and PostToolUse like any agent loop. So your hook configuration in settings.json applies to subagents too — if you have a PreToolUse hook blocking rm -rf, the subagent inherits that protection. The other direction (a hook calling a subagent) is also supported: hook handlers can be type: 'agent', which dispatches the matched event to a subagent for handling.
What's the migration path from CLAUDE.md instructions to Skills?
Pull procedural sections out of CLAUDE.md into Skills as soon as the procedure exceeds about 300 words or starts repeating across projects. CLAUDE.md is loaded every turn, so it's the right home for short, project-defining facts (architecture, conventions, gotchas). Skills are loaded on demand, so they're the right home for longer how-to procedures that don't need to be in every prompt. The rule of thumb most teams settle on: CLAUDE.md answers 'what is this project'; Skills answer 'how do I do X in this project'. Convert the long ones to Skills and replace them in CLAUDE.md with a one-line reference like 'See claude-code-review skill for the review checklist.'
Sources
Official Claude Code docs
- code.claude.com/docs/en/skills — Skills authoring and activation
- code.claude.com/docs/en/sub-agents — Subagents, frontmatter fields, Task tool
- code.claude.com/docs/en/plugins — Plugin manifest, marketplace, install flow
- code.claude.com/docs/en/hooks — full hook event list, settings.json schema, exit-code semantics
Open specs and cross-vendor
- github.com/agentskills/agentskills — open SKILL.md spec (Apache 2.0)
- github.com/anthropics/skills — Anthropic’s reference Skills
- github.com/vercel-labs/skills — cross-agent skills CLI
Related deep dives on this site
- /blog/claude-skills-vs-mcp-vs-subagents-vs-cli-2026-decision-matrix — Skills vs MCP vs Subagents vs CLI decision matrix (the sibling post)
- /blog/cross-agent-skills-cursor-codex-cline-antigravity-gemini-mastra-portability — portability of Skills across editors
- /blog/karpathy-claude-md-annotated-2026 — CLAUDE.md as the always-loaded sibling of on-demand Skills
Internal links