Extending Claude Code

Claude Code skills vs subagents vs hooks: what loads, what runs isolated, what breaks silently

Short answer

A skill is on-demand reference material: its description sits in context on every turn so Claude knows it exists, but the full body loads only when invoked — and once loaded, it stays in context for the rest of the session. A subagent is an isolated worker: it runs in its own context window with a fresh start, and only its summary returns to your conversation. A hook is a deterministic trigger that fires automatically on a lifecycle event — but "deterministic" describes when it runs, not whether it blocks anything: only an exit code 2 or an explicit JSON deny stops the action. Every other outcome, including a hook that silently fails to fire at all, is a no-op that looks configured.

Pick by what each mechanism costs you when you get it wrong, not by what it's called: a skill is a context-budget decision, a subagent is a blast-radius container, and a hook is a blast-radius bet that needs proof it actually pays off. This guide is the decision, built on the mechanics Anthropic documents and what this repo got wrong — and got right — putting them into practice.

What each one actually is

The three sit at different points in the same problem — how much of a Claude Code session's limited attention a piece of instruction or logic gets, and how contained the damage is if it misfires. Verified against Anthropic's own documentation, not memory:

What is a Claude Code skill?

Per Anthropic's skills reference, a skill is a directory at .claude/skills/<name>/SKILL.md (or ~/.claude/skills/ for personal, project-wide use). The directory name becomes a slash command; the frontmatter's description is what Claude reads to decide whether to invoke it unprompted. The field list is long — name, description, when_to_use, argument-hint, arguments, disable-model-invocation, user-invocable, allowed-tools, disallowed-tools, model, effort, context, agent, background, hooks, paths, shell, metadata — and only description is meaningfully required.

The load-bearing fact for context budget: a skill's description works like a short, always-on advertisement — present in context every turn — while the body is what you opt into by invoking it, either by typing /name or by Claude matching the description on its own. From that point on, the body is injected once as a conversation message; it is never re-read, and it does not leave until compaction — re-invoking an unchanged skill just gets an "already loaded" note rather than a second copy. Custom commands (.claude/commands/*.md) merged into the same mechanism.

What is a subagent?

Per Anthropic's subagents reference, a subagent is a specialized assistant defined at .claude/agents/<name>.md, with frontmatter for name, description (which drives automatic delegation), tools, model, permissionMode, and more. When Claude — or you — dispatches one, it runs in its own separate context window: a custom system prompt, the delegation task, CLAUDE.md at every level (except the built-in Explore and Plan agents, which skip it deliberately to stay cheap), and nothing else from the main conversation. It returns only a summary. That isolation is the point: a broad exploration that would fill your main window with file dumps instead fills a disposable one, and a subagent's tool access can be scoped down to exactly what the task needs.

What is a hook?

Per Anthropic's hooks reference, a hook is a shell command, HTTP endpoint, or LLM prompt registered in settings.json (user, project, or local scope) that fires automatically on a lifecycle event — PreToolUse, PostToolUse, Stop, SessionStart, and roughly two dozen others. The part that catches people: hooks are advisory by default, not a firewall. Exit code 0 with no output means "no decision" — the action proceeds through normal permissions, a silent hook does not approve anything, and it does not deny anything either. Only exit code 2, or a JSON permissionDecision: "deny", actually blocks. Exit code 1 — the conventional Unix failure code — is not blocking unless paired with valid deny JSON. And a hook that can't even start — a mistyped path, a missing script — fails into the same non-blocking bucket, with a notice easy to miss on first run.

The context-budget decision

What a mechanism costs before you ever use it — the tax on every single turn, whether or not the task needs it:

MechanismWhat's in context by defaultWhen the rest loadsDoes it persist?
SkillDescription only (short).Full body, on invocation.Yes — stays for the session once loaded; not re-read per turn.
SubagentNothing, until dispatched.Runs in its own window entirely; only the summary reaches you.No — the working context is thrown away when it returns.
HookNothing — hooks aren't prompt content.Never loads text into context; it runs code and can inject a short additionalContext string.N/A — it's config, not conversation.

The practical read: a skill you invoke often and never need again this session is a standing tax on every subsequent turn — there's no "unload" short of compaction dropping it. A subagent is the opposite shape: expensive per-call (a fresh context has to reconstruct everything it needs), cheap in steady state (nothing lingers). A hook is nearly free either way, which is exactly why it's tempting to over-rely on one as your enforcement layer — see below.

The blast-radius decision

What each mechanism can break, and how far the damage travels:

  • A skill's damage is contained to its own advice. It's prompt content — the worst case is bad instructions Claude follows in the main conversation, which you can see and correct in the same window. The exception is allowed-tools: a skill can pre-approve tool calls for the turn that invokes it, so a skill checked out from a repo you don't control can grant itself real tool access before you've read it.
  • A subagent's damage is contained by construction, but not eliminated. Isolation keeps a runaway exploration or a verbose failure out of your main window. It does not, on its own, stop a subagent from writing files or running commands — that's still governed by its tools/permissionMode and by what the parent session's permission mode allows it to inherit.
  • A hook's damage runs the opposite direction: not doing too much, but silently doing nothing. A hook that never fires, or fires and doesn't block, looks identical from the outside to a hook working correctly — there's no error, no red mark, nothing to notice until you go looking for the thing it was supposed to have stopped.

As built: two wrong calls and the boundary that held

The vendor docs describe the mechanics correctly. What they don't carry is the failure mode you only see at repo scale, over weeks, with real consequences — and one case where the safeguards held up under the same scrutiny. From this build:

The 23-of-25 skill demotion — a context-budget mistake

This repo vendors a set of video-rendering skills (hyperframes) for producing motion-graphics content. All 18 of them lived in .claude/skills/ at first, which meant all of their descriptions preloaded into every session — release runs, content edits, unrelated bug fixes — regardless of whether that session would ever touch video. All 18 were demoted out to media/hyperframes/skills/, a directory Claude Code doesn't scan by default. Two days later, faceless-explainer and general-video were pulled back into .claude/skills/ — not because the original demotion was wrong, but because YouTube became active work and the "sees zero Now/Next use" premise had stopped holding for exactly those two. The rest stayed demoted, and a 2026-08-12 refresh grew the whole family from 18 to 25 skills, installed by hand into the demoted directory: 23 demoted, the same 2 promoted. The fix cost nothing functionally — a skill still works once pulled back in for a specific video job — it just stopped being a standing tax on every session that never needed it. The lesson generalizes past this repo: a skill's context cost is paid on every turn of every session where its description loads, not just the sessions that use it — and the right response to the premise expiring for one item is restoring that item, not reopening the whole gate.

Two hooks that never fired once — a blast-radius mistake

Two PreToolUse hooks in this repo's .claude/settings.json inject a knowledge-graph context note before Bash commands and file reads (matchers Bash and Read|Glob), gated on a check that the graph index exists. The gate read [ -f .graphify/graph.json ]. The graph has never lived at that path — the real build output is graphify-out/graph.json. From the day those hooks were written until the mismatch was caught and repointed, neither hook had ever fired once. Nothing signaled that: no error, no warning in a transcript, no gap in output that read as a gap rather than as "the feature just doesn't apply here." A hook silently mis-gated reads exactly like a hook that correctly decided not to fire — that's the failure mode the vendor docs' "advisory by default" language is describing, and it's easy to read as an abstract caveat until it costs a repo two working guardrails for weeks with nobody the wiser. The fix (current state, verified): both hooks now check graphify-out/graph.json.

A deny list, a model tier, and reviewers that can't write — three ways this repo bounds a subagent's blast radius

Isolation alone doesn't answer "what is this subagent allowed to actually change." This repo layers three separate mechanisms on top of it, each closing a different opening:

  • A path deny list. .claude/settings.json denies Edit and Write on ./.agents/** — a directory that belongs to a different coding agent's own skill tree, gitignored and not ours to touch. The deny is exactly those two tools: any subagent's Edit or Write call against that path is blocked regardless of its own frontmatter, but the permission system doesn't reach a Bash command writing the same bytes — the deny list closes one door, not the room.
  • A model tier by task shape, not convenience. This repo's dispatch rules pin opus to subagents whose job is to decide — verdicts, lane calls, prioritisation calls — and default everything else to sonnet; a subagent that only needs to stamp a ledger row or move a table entry runs haiku. The point isn't cost alone: it's matching the model's judgment capacity to whether the task exercises judgment, so a mechanical sweep doesn't quietly acquire opinions it wasn't asked for.
  • Reviewers bounded by instruction, and — for one of the five — by tool grant too. Every review-only subagent in this repo carries the same line in its prompt: "You cannot message anyone and you cannot edit. Your verdict reaches the author only" through whatever dispatched it. Only lib-boundary-reviewer is actually held to that by its tool grant — Read/Grep/Glob and nothing else. The other four also carry tools that could write or reach out: internal-canon-reviewer and product-claims-reviewer hold Bash, model-id-currency and plagiarism-reviewer hold WebSearch/WebFetch. Nothing stops a Bash-holding reviewer from editing a file or calling out, except the prompt telling it not to — and that gap is exactly the point of a blast-radius guide: an instruction a subagent could ignore is a different guarantee than a tool grant that makes the action impossible, and four of these five reviewers rest on the former.

When each is the wrong choice

MechanismWrong when
SkillThe action has side effects you don't want Claude deciding to trigger on its own (deploys, sends, destructive commands) — a skill's default is that Claude can invoke it; use disable-model-invocation or gate it behind a hook, don't rely on the description alone to hold Claude back. Also wrong as your only enforcement of a "must always hold" rule — a skill is prose Claude can drift from.
SubagentThe task is small and needs the main conversation's own context to answer cheaply — spinning up an isolated worker for a one-line lookup pays a reconstruction cost that outweighs what isolation buys you. Also wrong when the subagent's output has to reconcile against state it structurally cannot see (a decision already logged elsewhere, a sibling's concurrent edit) and nothing after it checks that reconciliation by hand.
HookYou're treating "I configured a hook for this" as proof the rule holds. Until you've verified the exact exit-code path fires — a canary run, not a read of the config — a hook gating something load-bearing is a claim, not a control. Also wrong for anything that needs human judgment mid-decision: a hook is a yes/no gate at a fixed point, not a reviewer.

Frequently asked questions

What's the difference between a Claude Code skill and a subagent?

A skill is prompt content that loads into your existing conversation — the same context window, the same history, the same model unless overridden. A subagent runs in a completely separate context window with no access to your conversation history, and only its final summary comes back. A skill can itself run as a subagent by setting context: fork in its frontmatter, which is the one place the two mechanisms combine directly.

Do Claude Code hooks actually enforce rules, or just suggest them?

Neither, by default — a hook is advisory unless it explicitly returns a blocking result. Exit code 0 with no output is "no decision," not approval; only exit code 2, or JSON with permissionDecision: "deny", blocks the action. A hook that times out, can't start, or exits with any other code proceeds as if it hadn't run at all. If you need a rule to always hold, verify the hook actually reaches the blocking path — don't assume the config is the control.

Does a Claude Code subagent see my project's CLAUDE.md?

Yes, at every directory level — with one deliberate exception: the built-in Explore and Plan agents skip CLAUDE.md (and git status) deliberately, so exploration stays a cheap read. Any custom subagent you define loads it like the main session does, so repo-specific rules reach a custom subagent automatically, but a rule you need enforced during exploration has to be restated directly in the delegation prompt.

How much context does a Claude Code skill actually cost?

Its description costs a small, fixed amount on every turn, whether or not it's ever used — that's what lets Claude decide when to invoke it. The full body costs nothing until invoked, but once invoked it stays loaded for the rest of the session (or until auto-compaction drops it under budget pressure). A skill you invoke once early and never again is dead weight for every turn after that, which is why a low-frequency skill belongs somewhere it isn't preloaded by default.

Sources & provenance
  • Mechanics — skill frontmatter, loading behavior, and content lifecycle: Anthropic, "Extend Claude with skills" (code.claude.com/docs).
  • Mechanics — subagent isolation, context inheritance, Explore/Plan exception: Anthropic, "Subagents in Claude Code" (code.claude.com/docs).
  • Mechanics — hook events, blocking semantics, exit-code behavior: Anthropic, "Hooks reference" (code.claude.com/docs).
  • As-built evidence, this platform's own build: the hyperframes skill demotion and the graphify hook path mismatch (CLAUDE.md §Stack and §Graph tools), the current hook gate (.claude/settings.json), the .agents/ deny list (.claude/settings.json), the model-tier rule (docs/agents/DISPATCH.md), and the review-agent write boundary (.claude/agents/internal-canon-reviewer.md).

Claude Code's own mechanics move fast — verify frontmatter fields and blocking semantics against the live docs before relying on them for anything load-bearing. Corrections: hello@aiarch.dev.

Learn to build agentic systems where the failure mode is the lesson, not the surprise.

aiArch teaches context management, subagent isolation, and deterministic gating as first-class engineering decisions — on a platform that logs its own mistakes with these exact mechanisms rather than pretending they didn't happen.

Free sample — no signup · every claim cited · full curriculum is waitlist-only

Subscribe to the Brief — free. This is the newsletter, not the membership waitlist — request an invite here →