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

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

SurfaceLives atResident (every session)Loads on useRuns in
Skill.claude/skills/<name>/SKILL.md (+ user, plugin scopes)description line (capped 1,536 chars)body, then bundled filesyour window
Slash command.claude/commands/*.md (merged into skills)discoverable namebody on /nameyour window
Subagent.claude/agents/*.mdname + descriptionits system promptits own window
Hooksettings.jsonnothingstdout, on 3 events onlyyour shell
MCP serverclaude mcp add (local/project/user)names + server info; schemas deferred by tool searchschemas on discovery, results per callserver process
CLAUDE.mdproject/user hierarchyeverything, imports expandedn/ayour 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:

  1. 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: true loads nothing until you type /name: a zero-rent skill.
  2. Invocation: the body loads. The docs' size guidance is explicit: keep SKILL.md under 500 lines and move reference material to separate files.
  3. 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 addPut it inWhy
A rule that must always hold ("never push to main")hookdeterministic, zero tokens, survives compaction by construction
A convention Claude should reason with ("prefer pathlib")CLAUDE.md, one linejudgment calls need to be in context; keep the line short, it is rent
A multi-step procedure used sometimes ("release process")skill30-token rent, body on use, /name invocable
A task worth isolating (heavy search, review)subagentits tokens spend in another window; you pay a description
A capability from an external systemMCP serverdeferred by tool search; prune the ones you stopped using
A fact ("repos ledger, API notes")pointer + file, or memoryChapter 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.md fully 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.md layer 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.