TLDR; Claude Code has four extensibility primitives, each for a different job: commands are user-triggered prompt shortcuts, skills load domain expertise on demand, hooks fire automatically on system events, and agents delegate work to an isolated context. Picking the wrong one is the most common mistake; match the primitive to what you actually need.
Claude Code ships with four ways to extend its behaviour. Most people discover them one at a time, in no particular order, and end up using the wrong one for the job. I did this myself for months, writing skills where a hook would have been simpler, building elaborate commands when I needed an agent.
The distinction isn’t obvious until you’ve built with all four. Skills, hooks, agents, and commands each solve a different problem, and picking the wrong one produces something that works initially and then becomes a tangle of workarounds. This guide is the decision map I wish had existed when I started.
How the four primitives differ
Before the details, one table to orient:
| Primitive | What it does | Triggered by | Runs in |
|---|---|---|---|
| Command | Injects a prompt template | User types /name |
Main context |
| Skill | Loads domain expertise on demand | Claude’s judgement or /name |
Main context |
| Hook | Fires a command, request, tool call, or model check on an event | System events (automatic) | Outside the main conversation |
| Agent | Delegates a subtask to a separate Claude instance | Claude’s judgement or skill config | Isolated context |
Commands and skills expand what Claude knows. Hooks and agents expand what Claude does automatically. Once that distinction is clear, the choice becomes straightforward.
Skills: reusable expertise
A skill is a structured prompt with domain knowledge, loaded on demand. This is the most important distinction from CLAUDE.md: Claude loads skill descriptions at startup so it can decide when to use them, but the full skill body and supporting references stay out of context until the skill is invoked.
This site’s CLAUDE.md is about 215 lines, down from about 740 once I moved its subsystem detail into path-scoped rules. But I run complex workflows across Rails applications, Swift iOS builds, marketing campaigns, and infrastructure deployments. All of that domain knowledge lives in skills, not in the file that loads every session.
Anatomy of a skill
A skill lives in a directory with a SKILL.md file:
~/.claude/skills/
└── tech-lead/
├── SKILL.md
├── references/
│ ├── task-decomposition.md
│ └── prd-template.md
└── examples/
└── complete-prd.md
The SKILL.md has YAML frontmatter and a markdown body:
---
name: tech-lead
description: Use when asked to 'plan a feature', 'write a PRD',
or 'implement a feature'.
---
# Tech Lead Skill
Research, plan, and execute features end-to-end...
The description field is what Claude reads to decide whether to load the skill. When you say “plan this feature,” Claude scans skill descriptions across your personal (~/.claude/skills/), project (.claude/skills/) and plugin skills, finds the match, and loads the full SKILL.md into context. The references/ and examples/ directories stay out of context until the skill explicitly reads them, which is the progressive disclosure that keeps this efficient.
When to write a skill vs a good prompt
Write a skill when you find yourself repeating the same multi-step workflow across sessions. My tech-lead skill triages a request into one of three lanes, then runs whichever of its phases that lane needs: market research, PRD writing, task decomposition, agent delegation, code review, and merge. There’s no way I’m typing that as a prompt each time.
Not everything needs a skill, though. If the instruction is a one-liner (“always use conventional commits”), put it in CLAUDE.md. If it’s a single-use prompt, just type it. Skills earn their keep through repetition and structure, not through coverage.
A skill can also wrap a local tool. My open-source /transcribe skill runs speech recognition on the Mac and hands a speaker-labelled transcript back to Claude for the next step.
Token economics
That CLAUDE.md costs roughly 3,000 tokens every session. My rails-developer skill, with its references on model patterns, controller conventions, Hotwire guides, and DaisyUI components, runs to about 8,000 tokens when fully loaded. If that lived in CLAUDE.md, I’d pay for it during Swift sessions, marketing sessions, every session. As a skill, it loads only when I’m doing Rails work.
Across a typical week with 50+ sessions, that compounds. Skill-based architecture versus everything-in-CLAUDE.md is the difference between paying for what you use and paying a flat tax on every conversation.
Hooks: automated guardrails
Hooks are fundamentally different from skills and commands. They don’t add knowledge to Claude; they run shell commands or prompts in response to system events, automatically, without requiring you to remember to trigger them.
I think of hooks as quality gates that operate whether or not you’re paying attention.
How hooks work
You configure hooks in settings.json with an event name and one or more actions:
{
"hooks": {
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "git diff --quiet || (bin/rubocop -a; bin/rails test); exit 0",
"statusMessage": "Running linters and tests after agent completed"
}
]
}
]
}
}
This fires every time a subagent finishes work. If the agent changed any files, it runs RuboCop auto-fix and the test suite. No prompting required, no remembering to ask Claude to check its work.
The event model
Claude Code exposes events across the full session lifecycle. These are the events I use most, among 30-plus:
- PreToolUse / PostToolUse: before and after Claude calls any tool (Bash, Edit, Write, etc.)
- Stop: when Claude finishes responding
- SubagentStop: when a subagent completes its work
- SessionStart / SessionEnd: session lifecycle
- UserPromptSubmit: before your message reaches Claude
The most useful pattern is PreToolUse with a matcher. You can intercept specific tools before they execute:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/check-dangerous-commands.sh"
}
]
}
]
}
}
A hook that exits with code 2 blocks the tool execution entirely and feeds the error message back to Claude. Exit 0 allows it. This is how you enforce hard rules, no force pushes, no rm -rf on certain paths, no committing .env files, without relying on Claude to remember them.
Hook types
Hooks come in five types:
- Command: runs a shell script. This covers 90% of cases.
- HTTP: POSTs to an endpoint. Useful for external integrations.
- Prompt: sends a one-turn prompt to an LLM for a yes/no decision.
- Agent: spins up a subagent for more complex verification.
- MCP tool: calls a tool on an already-connected MCP server, no wrapper script needed.
The memory vault plugin I use runs a Python script on the Stop event that nudges me to save knowledge at the end of a session. Simple, reliable, and it runs without me thinking about it.
When hooks beat instructions
If you find yourself writing “NEVER do X” in CLAUDE.md, that’s usually a sign you need a hook instead. CLAUDE.md instructions are suggestions; Claude follows them well, but not perfectly. A PreToolUse hook that blocks a dangerous command is a guarantee. The instruction discourages. The hook prevents. My open-source plugin hamster automates that move: it turns a mistake Claude keeps repeating into a hook rule that must pass its own tests.
Agents: autonomous delegation
Agents are separate Claude instances that work on subtasks in their own context window. This is the primitive that changed how I structure complex work.
Agents provide context isolation. When I ask Claude to explore a large codebase, the main conversation doesn’t get polluted with hundreds of file reads. The agent does the exploration, summarises the findings, and returns a focused result, usually 1,000 to 2,000 tokens, while the agent’s full context is discarded.
Built-in agents
Claude Code ships with several agent types you can use immediately:
- Explore: read-only codebase research. Can search files, read code, and grep, but can’t edit anything. Fast and safe for orientation tasks.
- Plan: architecture and design. Can read code and analyse, but can’t write. Returns implementation plans.
- general-purpose: full access to Claude Code’s tools. Use when the subtask needs to build or change things.
Custom agents
You define custom agents as markdown files with frontmatter that controls their capabilities:
---
name: knowledge-searcher
description: Deep multi-entry vault searches when /recall isn't sufficient.
model: sonnet
maxTurns: 20
tools: Bash, Read, Glob, Grep
---
Search the knowledge vault for entries matching the query.
Break complex queries into multiple search terms and
synthesise findings from multiple entries.
The tools field is a whitelist; this agent can only use Bash, Read, Glob, and Grep. It can’t edit files, write new ones, or spawn its own agents. You control the blast radius.
Custom agents can live at project scope in .claude/agents/, user scope in ~/.claude/agents/, or inside a plugin’s agents/ directory. The memory vault plugin I use has three: a knowledge-searcher for deep vault queries, a knowledge-extractor that captures findings from a session into persistent storage, and a vault-curator for periodic hygiene. Each has a focused purpose and constrained toolset.
Briefing agents effectively
The most common mistake with agents is under-briefing them. A subagent gets the task you send it, not the main conversation transcript. Custom subagents load your CLAUDE.md files but not your auto memory, and the built-in Explore and Plan agents start without CLAUDE.md. None of those layers tells an agent what you tried five minutes ago or why this particular task matters. Brief it like a colleague who knows the project but just walked into this conversation:
Bad: “Fix the failing test.”
Good: “The test in test/models/user_test.rb:45 fails with NoMethodError: undefined method 'full_name'. The User model at app/models/user.rb was refactored in the last commit to rename full_name to display_name. Update the test to use the new method name and verify it passes.”
The second brief gives the agent everything it needs without re-deriving context you already have.
Background and worktree isolation
Subagents run in the background by default; Claude runs one in the foreground only when it needs the result before continuing. Custom agents can set background: true in frontmatter, and you can explicitly request background execution for independent work. Background agents notify the parent when they finish and return their result to the main conversation. They can also run in isolated git worktrees, which means their file changes don’t touch your working directory until you explicitly merge them. This is how I handle parallel implementation tasks: spin up three agents in worktrees, each working on a different feature branch, and review the results when they’re done.
Commands: user shortcuts
Commands are the simplest extensibility primitive. A markdown file in ~/.claude/commands/ that you invoke with a slash:
# ~/.claude/commands/next-task.md
Pick up the next available task from the current task list.
## Current State
!`git branch --show-current`
!`git status --short | head -10`
!`git log --oneline -3`
## Steps
1. Run TaskList to see all tasks and their statuses
2. Find the lowest-ID task that is pending with no unresolved blockers
3. Create the feature branch and start work
The !`backtick` syntax runs those shell commands before the prompt reaches Claude. By the time Claude sees this, the git status and branch name are already filled in. Preprocessing, not a tool call.
I type /next-task and Claude picks up where I left off. No explanation needed, no remembering the workflow. The command carries the process.
Commands vs skills
Both are markdown files. Both get invoked with /name. Under the hood, current builds register them through the same mechanism, a flat commands/*.md and a SKILL.md both bind a /name trigger, so the real difference is structure and autonomy, not plumbing.
Commands are flat files in a commands/ directory: prompt templates with optional shell preprocessing. They’re always user-invoked. Claude won’t trigger a command on its own.
Skills are directories with a SKILL.md, supporting references/ and examples/, and the same frontmatter as a command file, plus name and paths. What they add over a command is automatic loading: Claude can invoke one based on its description field.
If the extension is a simple “do this sequence of steps,” write a command. If it needs domain knowledge, supporting materials, or should trigger automatically, write a skill.
My /review-session is a command: it runs a fixed retrospective process. My tech-lead is a skill: it carries a full PRD methodology with templates, task decomposition logic, and delegation rules that Claude invokes automatically when I say “plan this feature.” (It started life as a loose skill in ~/.claude/skills/; it now ships inside a plugin, which is where any workflow you use across projects eventually wants to live.)
Plugins: packaging it all together
Once you have skills, hooks, agents, and commands that work well together, a plugin bundles them into a single installable package:
memory-vault/
├── .claude-plugin/
│ └── plugin.json # Manifest
├── skills/
│ └── knowledge-retrieval/
│ └── SKILL.md
├── agents/
│ ├── knowledge-searcher.md
│ └── knowledge-extractor.md
├── commands/
│ ├── recall.md
│ └── remember.md
├── hooks/
│ └── hooks.json
└── scripts/
├── search-vault.py
└── capture-knowledge.py
The plugin.json manifest is minimal:
{
"name": "memory-vault",
"version": "0.1.0",
"description": "Long-term cross-project knowledge management"
}
Everything inside the plugin works exactly as it would standalone: skills match by description, hooks fire on events, agents spawn on demand, commands respond to /name. The plugin provides packaging and namespacing. Plugin skills get invoked as /memory-vault:knowledge-retrieval to avoid name collisions across plugins.
Local vs marketplace
Plugins can be local (a directory on your machine) or distributed through a marketplace (a git repository with a marketplace.json listing). I keep my custom plugins in a local marketplace at ~/.claude/marketplaces/local-plugins/ and enable them per-project in settings.json. Official plugins from Anthropic, Playwright for browser automation, Ruby LSP for language intelligence, come through the official marketplace.
A plugin skill and a loose global skill can share a short name while still resolving through different namespaces and precedence rules. That makes invocation and debugging needlessly ambiguous. When a skill graduates from ~/.claude/skills/ into a plugin, delete the loose copy or give the two versions distinct names.
My memory vault plugin combines a Stop hook (nudges to save knowledge), three agents (deep search, extraction and curation), eight commands (recall, remember, related, browse, curate, gaps, graph, status), and a skill (knowledge retrieval context). None of these primitives does much alone. Together, they create a persistent knowledge system that operates across every session.
Choosing the right primitive
When you need Claude Code to do something new, work through this:
Process with multiple steps and domain knowledge? Write a skill. It loads on demand, carries supporting materials, and Claude can invoke it automatically.
Quick shortcut you trigger manually? Write a command. Flat file, slash invocation, optional shell preprocessing.
Something that should happen automatically when an event occurs? Write a hook. Command, HTTP, and MCP-tool hooks can run deterministic work without another model turn. Prompt and agent hooks deliberately call a model when the decision needs judgement.
Work that needs to run in a separate context without polluting your conversation? Use an agent. Isolated context, constrained tools, focused output returned to your main thread.
Several of these bundled for reuse across projects? Build a plugin. Package skills, hooks, agents, and commands with a manifest.
A hard rule Claude must never violate?
Write a PreToolUse hook that blocks the action, not a CLAUDE.md instruction that suggests against it.
Automated tests after changes?
Write a PostToolUse or SubagentStop hook that runs your test suite, the same mechanism the quality gates guide builds on for automated review and fact-checking.
Specialised knowledge that should only load when relevant?
Write a skill with references/ for the heavy content. It stays out of context until invoked.
The arrangement I’ve settled on:
- CLAUDE.md for universal rules (under 200 lines)
- skills for domain expertise (full instructions loaded on demand)
- hooks for automated quality gates (deterministic where possible, model-backed where useful)
- agents for delegated subtasks (isolated working context)
- commands for personal shortcuts (simple triggers)
Each primitive in its lane.
Agents I build for Applied AI clients run with limited permissions, and their hard limits sit in code the model cannot skip.
Related reading
- Top 10 ways to optimise Claude Code token usage, how skills and agents reduce token costs
- How I use Claude Code to manage six client projects, the full workflow these primitives support
- Claude Code hook examples: turning mistakes into tested rules, hooks and skills packaged as an open-source plugin
- Local audio transcription with speaker labels in Claude Code, a skill that wraps a local tool
Common questions
What is the difference between skills, hooks, agents and commands in Claude Code?
Skills are reusable expertise the model loads when relevant; hooks are automated guardrails the harness runs on events; agents are autonomous delegates for scoped work; commands are user shortcuts you trigger. Skills and agents extend capability, hooks enforce behaviour, commands save typing.
When should you use a hook instead of a skill?
Use a hook when something must happen automatically on an event, because the harness runs hooks, not the model. Use a skill when you want expertise available on demand for the model to apply. Automatic-every-time is a hook; apply-when-relevant is a skill.
How do you use hooks in Claude Code?
Add them to settings.json under an event, such as PreToolUse, PostToolUse, Stop or SubagentStop, with an optional matcher and an action, usually a shell command. A PreToolUse hook on Bash can check each command before it runs: exit code 2 blocks it and feeds its error message back to Claude, exit 0 lets it through. I use a SubagentStop hook that runs the linters and tests when a subagent finishes and has changed files.
In what order should you learn these four?
Most people discover them in the wrong order and over-build. I start with commands and skills for everyday leverage, add hooks when a rule must be enforced automatically, and reach for agents when work needs autonomous delegation. Plugins package the set once it stabilises.