The extension surfaces: skills, hooks, agents, MCP, and their token contracts
TL;DR. Every way of extending Claude Code signs a different token contract, and knowing the six contracts is the difference between a setup that scales and one that eats its own window. Skills load only their metadata each session (descriptions, truncated at 1,536 characters each) and pay for their body on invocation; subagents advertise a description and spend their real tokens in a separate window; hooks are shell commands outside the model, zero resident tokens, with only three events whose stdout enters context; slash commands are skills now (officially merged); MCP servers used to dump every tool schema into the prompt until tool search made them deferred by default past a 10%-of-window threshold; and CLAUDE.md remains the one surface that loads in full, always. A live audit of this machine prices the real configuration: 86 resident tokens buying access to an 11,133-token skill, a 183-token agent listing, a zero-token hook, and a CLAUDE.md layer that costs more than all extensions combined. The design rule that falls out: metadata is rent, bodies are purchases, hooks are free, and the biggest wins come from moving behavior down the contract ladder.
Contents
- Six surfaces, six contracts
- The lab: this machine, audited live
- Skills: progressive disclosure as a product feature
- Hooks: behavior with no token cost at all
- Subagents: descriptions here, tokens elsewhere
- MCP and tool search: the heavy surface, tamed
- Where to put a behavior
- Further reading
- Takeaways
Chapter 18 mapped the memory layers and Chapter 40 priced the loading rules; this chapter applies the same lens to the behavior layers. Doc details below were verified against the live Claude Code documentation in July 2026; thresholds and caps drift, so treat the docs as the authority and this chapter as the map.
Six surfaces, six contracts
| Surface | Lives at | Resident (every session) | Loads on use | Runs in |
|---|---|---|---|---|
| Skill | .claude/skills/<name>/SKILL.md (+ user, plugin scopes) | description line (capped 1,536 chars) | body, then bundled files | your window |
| Slash command | .claude/commands/*.md (merged into skills) | discoverable name | body on /name | your window |
| Subagent | .claude/agents/*.md | name + description | its system prompt | its own window |
| Hook | settings.json | nothing | stdout, on 3 events only | your shell |
| MCP server | claude mcp add (local/project/user) | names + server info; schemas deferred by tool search | schemas on discovery, results per call | server process |
CLAUDE.md | project/user hierarchy | everything, imports expanded | n/a | your window |
Read the table once and the ladder is visible: CLAUDE.md is maximally resident, hooks are
maximally free, and everything else buys discoverability with metadata while deferring the
body. That is Chapter 40's loading ladder, rebuilt by the
product team as architecture.
The lab: this machine, audited live
"""The surfaces audit: what your Claude Code extensions cost, scanned live.
Claude Code has six extension surfaces, and each has a different token
contract: some put words in the window on every session (resident), some only
when used (lazy), and one runs entirely outside the model (free). This script
scans the real configuration on this machine, both scopes (user ~/.claude and
the current project's .claude), and prices what it finds with the book's
chars/4 estimate:
skills metadata (frontmatter name+description) resident every session;
the body loads on invocation; bundled files load on demand
agents description resident (the delegation menu); the system prompt
in the body loads only inside the subagent's own window
commands discoverable by name; the body loads when you type /name
hooks shell commands in settings.json: ZERO resident tokens; only
what they print enters context, at the moment they print it
MCP servers tool schemas load into context once connected: the heavy one
CLAUDE.md resident in full, imports expanded (chapter 18)
Standard library only. Run: python3 surfaces_audit.py
"""
import json
import re
from pathlib import Path
def toks(s):
return len(s) // 4
def frontmatter(text):
m = re.match(r"---\n(.*?)\n---\n(.*)", text, re.S)
return (m.group(1), m.group(2)) if m else ("", text)
SCOPES = [("user", Path.home() / ".claude"), ("project", Path.cwd() / ".claude")]
rows = []
for scope, root in SCOPES:
for skill in sorted(root.glob("skills/*/SKILL.md")):
meta, body = frontmatter(skill.read_text(errors="replace"))
extras = sum(f.stat().st_size for f in skill.parent.rglob("*")
if f.is_file() and f.name != "SKILL.md")
rows.append(("skill", scope, skill.parent.name,
toks(meta), toks(body) + extras // 4))
for agent in sorted(root.glob("agents/*.md")):
meta, body = frontmatter(agent.read_text(errors="replace"))
rows.append(("agent", scope, agent.stem, toks(meta), toks(body)))
for cmd in sorted(root.glob("commands/*.md")):
meta, body = frontmatter(cmd.read_text(errors="replace"))
rows.append(("command", scope, "/" + cmd.stem, toks(meta), toks(body)))
settings = root / "settings.json"
if settings.exists():
hooks = json.loads(settings.read_text()).get("hooks", {})
for event, entries in hooks.items():
n = sum(len(e.get("hooks", [])) for e in entries)
rows.append(("hook", scope, event, 0, 0)) if n else None
claude_json = Path.home() / ".claude.json"
if claude_json.exists():
cfg = json.loads(claude_json.read_text(errors="replace"))
servers = dict(cfg.get("mcpServers", {}))
servers.update(cfg.get("projects", {}).get(str(Path.cwd()), {})
.get("mcpServers", {}))
for name in sorted(servers):
rows.append(("mcp", "config", name, -1, 0)) # schema size set live
def claude_md_tokens(path):
"""A CLAUDE.md loads in full, with @imports expanded (one hop here)."""
if not path.exists():
return 0
text = path.read_text(errors="replace")
total = toks(text)
for imp in re.findall(r"^@(\S+)", text, re.M):
target = (path.parent / imp).expanduser()
if target.exists():
total += toks(target.read_text(errors="replace"))
return total
print("=== Extension surfaces on this machine, scanned live ===")
print(f"{'surface':<10}{'scope':<9}{'name':<21}{'resident tok':>13}{'lazy tok':>10}")
print("-" * 63)
for surface, scope, name, res, lazy in rows:
res_s = "at connect" if res < 0 else f"{res:,}"
print(f"{surface:<10}{scope:<9}{name:<21}{res_s:>13}{lazy:>10,}")
for scope, path in [("user", Path.home() / ".claude/CLAUDE.md"),
("project", Path.cwd() / "CLAUDE.md")]:
print(f"{'CLAUDE.md':<10}{scope:<9}{'(imports expanded)':<21}"
f"{claude_md_tokens(path):>13,}{0:>10,}")
print("-" * 63)
resident = sum(r[3] for r in rows if r[3] > 0)
lazy = sum(r[4] for r in rows)
print(f"""
{len(rows)} extension entries plus the CLAUDE.md layer. Extension metadata
riding in every session: ~{resident} tokens; deferred until used: ~{lazy:,}
tokens; hooks: 0 by construction. The shape to preserve as you add surfaces:
metadata is rent, bodies are purchases, hooks are free, and MCP schemas are
the one surface that bills like a body but loads like rent (check /context
after connecting a server; recent Claude Code versions defer large tool
inventories behind tool search for exactly this reason).""")
Verified output, this machine's real configuration:
=== Extension surfaces on this machine, scanned live ===
surface scope name resident tok lazy tok
---------------------------------------------------------------
skill user frontend-slides 86 11,133
agent user ask 183 649
hook user PreToolUse 0 0
CLAUDE.md user (imports expanded) 241 0
CLAUDE.md project (imports expanded) 1,897 0
---------------------------------------------------------------
3 extension entries plus the CLAUDE.md layer. Extension metadata
riding in every session: ~269 tokens; deferred until used: ~11,782
tokens; hooks: 0 by construction. The shape to preserve as you add surfaces:
metadata is rent, bodies are purchases, hooks are free, and MCP schemas are
the one surface that bills like a body but loads like rent (check /context
after connecting a server; recent Claude Code versions defer large tool
inventories behind tool search for exactly this reason).
The proportions are the lesson. One skill worth 11,133 tokens of instructions rides along
as an 86-token description: a 129x deferral ratio. The CLAUDE.md layer, at 2,138
tokens across both scopes, costs eight times all extension metadata combined, and it is the
only row with no lazy column at all. Whatever you add next, add it to a row with a lazy
column.
Skills: progressive disclosure as a product feature
A skill is a directory with a SKILL.md: YAML frontmatter plus a markdown body, with
optional supporting files beside it. Its token contract has three tiers, straight from the
docs:
- Session start: only the description loads ("one-line descriptions of available
skills so Claude knows what it can invoke"), with the description text capped at 1,536
characters per skill. A skill with
disable-model-invocation: trueloads nothing until you type/name: a zero-rent skill. - Invocation: the body loads. The docs' size guidance is explicit: keep
SKILL.mdunder 500 lines and move reference material to separate files. - On demand: bundled files load only when the skill's instructions send Claude to them. This is where the audit's 11,133 lazy tokens live.
Two operational details worth knowing. After auto-compaction, skill bodies are re-attached
"keeping the first 5,000 tokens of each" under "a combined budget of 25,000 tokens," most
recent first, so a monster skill can silently lose its tail across a compaction
(Chapter 42's probe recipes apply). And custom slash
commands have been formally merged into skills: "a file at .claude/commands/deploy.md and
a skill at .claude/skills/deploy/SKILL.md both create /deploy and work the same way,"
so the same contract governs both.
The design consequence: a skill is the correct home for any procedure you were tempted to
paste into CLAUDE.md. Moving a 2,000-token release checklist from CLAUDE.md (resident,
every session) into a skill (30-token description, body on use) is
Chapter 40's pointer migration with product support.
Hooks: behavior with no token cost at all
Hooks are shell commands the harness runs at lifecycle events, configured in
settings.json with matchers. The event list has grown to thirty (from SessionStart and
PreToolUse through PreCompact to SessionEnd); the token contract has not changed:
the rule itself never enters the window. The model does not know the hook exists; the
harness enforces it. Only three events' stdout is added as context Claude can see
(UserPromptSubmit, UserPromptExpansion, SessionStart); a PreToolUse hook exiting
with code 2 blocks the tool call and feeds its stderr back as the only tokens the exchange
costs.
This machine's one hook is this book's recurring example: RTK's PreToolUse matcher on
Bash rewrites commands to their compressed forms (Chapter 35),
enforcing a policy that would cost a standing CLAUDE.md instruction ("always prefix
commands with rtk...") for zero resident tokens, deterministically, with no risk of the
model forgetting. The general rule: any behavior that is a rule rather than a
judgment belongs in a hook, because a hook cannot be paraphrased away by compaction,
ignored under a long context, or charged per turn. Formatting after edits, blocking
dangerous commands, injecting a ticket number at session start: all free, all deterministic.
Subagents: descriptions here, tokens elsewhere
A subagent definition (.claude/agents/*.md) advertises name and description in the
main window, and that is all the main window ever pays: the docs are explicit that "each
subagent runs in its own context window," receives its own system prompt rather than the
main conversation, and "only the final summary comes back."
Chapter 13 priced this as the delegation lever; the surface view
adds the definition economics: the audit's ask agent costs 183 resident tokens to offer
a 649-token system prompt that executes in a different window entirely. The description is
load-bearing in both directions: it is what the main agent uses to decide when to delegate,
and it is the entire resident cost, so write it like the tool descriptions of
Chapter 28: specific about when, silent about how.
MCP and tool search: the heavy surface, tamed
MCP was the surface that broke the pattern: connecting a server historically loaded every
tool's full JSON schema into the prompt, resident, whether or not any tool was used. A
few generous servers could eat five figures of tokens before the first user message, which
is why Chapter 21's /context panel breaks MCP tools out as
their own line.
That is no longer the default story, and the fix is worth knowing by name: MCP tool
search. Since early 2026 (v2.1.7's changelog entry describes enabling it by default),
MCP tools are deferred: only tool names and server instructions load at session start,
and Claude discovers full schemas on demand through a search tool (ToolSearch) when a
task needs them. The documented threshold behavior: with ENABLE_TOOL_SEARCH=auto, "tools
load upfront if they fit within 10% of the context window, deferred otherwise" (an
auto:N variant tunes the percentage, and a per-server "alwaysLoad": true opts critical
servers out). Descriptions and server instructions are truncated at 2KB each.
Two consequences. First, the old advice "prune your MCP servers, every schema is rent" has
softened to "prune them anyway": deferred tools still cost their discovery round-trips, and
server instructions still ride along. Second, tool search is itself a beautiful instance
of this book's whole thesis: faced with a window-economics problem, the product team
reached for index-plus-retrieval, the same rung 3 of
Chapter 40's ladder that Serena's memories and the
MEMORY.md pattern occupy. The prompt holds a searchable index; the bodies load on
demand.
Don't be confused. Deferral changes when a schema is loaded, not what a tool call costs. Once a deferred tool is discovered and used, its schema and its results land in the conversation and are re-sent like everything else (Chapter 2), and a mid-session server toggle still invalidates the cache from the tools level down (Chapter 44, proof 3). Tool search fixes the resting cost of a big toolbox, not the marginal cost of using it.
Where to put a behavior
The decision table this chapter exists for:
| You want to add | Put it in | Why |
|---|---|---|
| A rule that must always hold ("never push to main") | hook | deterministic, zero tokens, survives compaction by construction |
| A convention Claude should reason with ("prefer pathlib") | CLAUDE.md, one line | judgment calls need to be in context; keep the line short, it is rent |
| A multi-step procedure used sometimes ("release process") | skill | 30-token rent, body on use, /name invocable |
| A task worth isolating (heavy search, review) | subagent | its tokens spend in another window; you pay a description |
| A capability from an external system | MCP server | deferred by tool search; prune the ones you stopped using |
| A fact ("repos ledger, API notes") | pointer + file, or memory | Chapter 40's ladder; facts are not behavior |
And the audit habit that keeps it honest: run /context after any surface change, run
surfaces_audit.py quarterly, and treat every resident token as a tenant that must justify
its rent at renewal (Chapter 45).
Remember. The six contracts are one principle wearing six coats: pay tokens in proportion to use, not to possession. Claude Code's own evolution keeps bending toward it (skills' progressive disclosure, subagent isolation, MCP deferral), and your configuration should bend the same way: rules to hooks, procedures to skills, isolation to subagents, facts to pointers, and only per-turn judgment left paying rent in
CLAUDE.md.
Further reading
- Claude Code docs: skills, hooks, sub-agents, MCP (
code.claude.com/docs/en/skills,.../hooks,.../sub-agents,.../mcp, including the "Scale with MCP tool search" section), and the context-window page that documents what loads when and what survives compaction. - Chapter 28: the injection channel these surfaces write into, and why descriptions steer behavior.
- Chapter 40: the loading ladder these contracts implement, priced.
- Chapter 35: the RTK hook this machine runs, dissected.
Takeaways
- Six surfaces, six token contracts:
CLAUDE.mdfully resident, skills metadata-resident and body-lazy (1,536-char description cap, 500-line guidance, 5k/25k post-compaction caps), subagents description-here-tokens-elsewhere, hooks zero-token by construction (stdout enters context on three events only), slash commands merged into skills, MCP deferred by tool search past a 10%-of-window threshold. - Audited live, this machine pays ~269 resident tokens for its extensions against ~11,800
deferred, while the
CLAUDE.mdlayer alone costs 2,138: the residency budget is usually spent where no lazy tier exists. - Rules go in hooks, judgment in
CLAUDE.md, procedures in skills, isolation in subagents, capabilities in MCP, facts in pointers. Pay for use, not possession. - Tool search is the book's thesis shipped as a product default: an index in the prompt, bodies on retrieval; it fixes the resting cost of a toolbox, not the marginal cost or the cache consequences of using it.
👉 With the surfaces priced, the measurement part that follows is where every such claim gets checked against receipts: the usage block, the cache machinery, and the ledger built from your own transcripts. Continue to The anatomy of a usage block.