DEV Community

Cover image for Claude Code Hooks vs Skills vs Subagents: Three Ways to Extend the Agent, and When Each Backfires
Ken Imoto
Ken Imoto

Posted on

Claude Code Hooks vs Skills vs Subagents: Three Ways to Extend the Agent, and When Each Backfires

The first time I tried to make Claude Code "always run the linter," I wrote a Skill. It worked most of the time. The rest of the time, Claude decided the task didn't need linting and skipped it. I spent an embarrassing afternoon tightening the Skill's description before I understood the actual problem: I had picked the wrong mechanism. "Always do X" is a Hook. "Decide whether to do X" is a Skill. They are not interchangeable, and the docs at the time were happy to let me confuse them.

Claude Code now ships four ways to extend the agent: Hooks, Skills, Subagents, and the Agent SDK. They overlap enough to look like alternatives and differ enough that using one where another belongs costs you reliability, tokens, or both. I've built real harnesses on the first three, and read the SDK docs closely enough to know where it takes over. Here is how each one actually fires, what it costs you in context, and the specific way each one bites back.

The one-line mental model

Before the table, the version I tell teammates:

  • Hooks are deterministic. They fire on an event whether Claude likes it or not.
  • Skills are probabilistic. Claude reads a description and decides to pull them in.
  • Subagents are isolation. They run in their own context window and hand you back a summary.
  • The Agent SDK is you taking the wheel: the same agent loop, running in your own process.

If you remember nothing else: Hooks remove a decision from the model, Skills add one, Subagents quarantine one.

Comparison of Claude Code Hooks, Skills, and Subagents across how each fires, its token cost, and when it backfires

The comparison

Mechanism How it fires Token cost Best for When it backfires
Hooks Lifecycle events (PreToolUse, Stop, etc.) defined in settings.json. Not the model's choice. Runs as a shell command outside the context window. Near zero, unless you deliberately inject stdout. Non-negotiable rules: format on write, block secret files, run tests before stop. A hook exiting 2 blocks the action. A bad Stop hook loops the agent forever.
Skills Claude matches your prompt against the Skill's description, or you type /name. Progressive disclosure: only name+description (a few dozen tokens) loaded per Skill at session start; body loads on invoke. Reusable procedures Claude should choose when relevant: a review workflow, a release-notes drafter. A bloated description taxes every turn and misroutes. The model can decline to fire at all.
Subagents Auto-delegated when a task matches the agent's description, or via the Agent tool (the old Task). Runs in its own context window. Only the final message returns to the parent. Quarantining noisy work: codebase-wide greps, log trawls, parallel exploration. It can't ask you follow-ups mid-task, and the summary drops the detail you needed.
Agent SDK Programmatic. You call query() from TypeScript or Python and drive the loop yourself. A separate program in your own process. Context is whatever your code feeds it. Productizing the agent: CI bots, backend services, anything outside the CLI. You own retries, sandboxing, session state, and cost. Nothing is automatic anymore.

Hooks: the mechanism that removes the model's vote

A Hook is a shell command wired to a lifecycle event in settings.json. The model does not get a say. This is the whole point and the whole danger.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "prettier --write \"$CLAUDE_FILE_PATHS\"" }]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

The 2026 event surface is much wider than the original set. Beyond the familiar PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, Stop, and Notification, there are now session events like SessionEnd, agent events like SubagentStart/SubagentStop, and context events like PreCompact/PostCompact (hooks reference).

The footgun is the exit code. Exit 0 is success. Exit 2 is a blocking error: stderr goes back to Claude and the action is rejected. On a PreToolUse hook that's a feature: you can stop Claude from touching .env. On a Stop or SubagentStop hook, exit 2 means "you are not allowed to stop, keep going." I once wrote a Stop hook that checked for uncommitted changes and exited 2 if it found any. The agent dutifully refused to stop, tried again, still had uncommitted changes, refused to stop again. I had built a loop with my own hands. The lesson: a Hook that can block is a Hook that can deadlock. Test the exit-2 path before you trust it.

Because Hooks run with your shell privileges and outside the context window, they cost almost nothing in tokens. That's why "always run the linter" belongs here and not in a Skill. You are not asking Claude to remember; you are removing the choice.

Skills: the mechanism that adds the model's vote

A Skill is a SKILL.md file with frontmatter. Claude reads the description of every installed Skill at session start and decides, per turn, whether your prompt warrants pulling one in. You can also force it with /name.

The mechanic that makes Skills scale is progressive disclosure. Three tiers load at three different times:

  1. Always loaded: the name and description only, a few dozen tokens per Skill. This is how one agent can know about hundreds of Skills without drowning.
  2. Loaded on invoke: the SKILL.md body. Anthropic's own guidance keeps the body under ~500 lines (skill-development SKILL.md).
  3. Loaded only when referenced: bundled scripts and detail files, zero tokens until Claude reaches for them.

So the failure mode is specific and quiet. Every Skill's description sits in context on every turn. Write a paragraph there and you've levied a permanent tax on the whole session, and you've made the routing decision harder, so the model misfires more often. My original "always lint" Skill failed because I was asking a probabilistic router to behave deterministically. The router did its job: it routed. Sometimes away from me.

Skills are right when you genuinely want Claude to choose. A code-review workflow, a "draft release notes from the git log" procedure, a triage routine. Things that should happen when relevant, judged by the model. Write the description like a routing key, not a summary (Agent Skills overview).

Subagents: the mechanism that quarantines context

A Subagent is a Markdown file in .claude/agents/ with frontmatter, and its body becomes that agent's system prompt:

---
name: codebase-explorer
description: Search the codebase for where a symbol is defined and used. Use proactively before refactors.
tools: Read, Glob, Grep
model: sonnet
---

You locate code. Report file paths and line numbers, not full file dumps.
Enter fullscreen mode Exit fullscreen mode

The defining property: it runs in its own context window. Its system prompt, its tool calls, every grep result and log line it reads, none of that enters your main conversation. Only the final message comes back (custom subagents). This is the single best tool for a task that would otherwise flood your context: "find every caller of this function across 400 files." Run it in a Subagent and you get back a clean list instead of 400 files of noise.

Two backfires bit me here. First, a Subagent can't stop to ask you a question. It runs to completion and reports once. If it hits an ambiguous decision halfway through, it guesses, and you find out from the summary. Second, that summary is lossy by design. The detail lived in the isolated window and is gone unless the agent thought to surface it. I've had a Subagent confidently report "no usages found" when it had quietly searched the wrong directory. The fix is to write the agent's instructions to return evidence (paths, counts), not just conclusions.

One 2026 gotcha worth flagging: permission modes are inherited from the parent and can override the Subagent's own frontmatter. If your main session is running with relaxed permissions, the Subagent's careful tools allowlist may not save you. Treat the tool list as scoping, not a security boundary.

Agent SDK: the mechanism that hands you the keys

When the agent needs to live outside the terminal, in CI, a backend service, a scheduled job, you reach for the Agent SDK. It was renamed from the Claude Code SDK because the same loop powers far more than coding (migration guide). It ships for both TypeScript (@anthropic-ai/claude-agent-sdk) and Python (claude-agent-sdk).

from claude_agent_sdk import query, ClaudeAgentOptions

async for message in query(
    prompt="Find and fix the failing test in auth.py",
    options=ClaudeAgentOptions(allowed_tools=["Read", "Edit", "Bash"]),
):
    print(message)
Enter fullscreen mode Exit fullscreen mode

You get the same agent loop, built-in tools, hooks, subagents, and MCP that power the CLI, but now running in your process. The backfire is everything the CLI used to do for free: retries, sandboxing, session persistence, and cost control are now your problem. One auth constraint to know before you build a product on it: third-party products on the Agent SDK can't use claude.ai subscription login, you authenticate with an API key (directly or via Bedrock/Vertex). Prototype on the SDK, then decide whether a hosted option fits production.

How I actually choose

The question I ask, in order:

  1. Must this happen every time, no exceptions? Hook.
  2. Should Claude decide when it's relevant? Skill.
  3. Is this going to dump a pile of junk into my context? Subagent.
  4. Does this need to run without me in the loop? Agent SDK.

Most of my mistakes came from answering question 2 when the honest answer was question 1. "Always" and "when relevant" feel similar when you're typing the config. They are opposites at runtime.

Takeaways

  • Hooks remove the model's choice. Use them for non-negotiable rules. Watch the exit-2 path: a blocking Stop hook can loop your agent.
  • Skills add a choice. Progressive disclosure keeps them cheap, but every description is taxed on every turn. Don't use a Skill to enforce something that must always happen.
  • Subagents quarantine context. Best for noisy exploration. They can't ask follow-ups and the summary is lossy, so make them return evidence.
  • The Agent SDK hands you the keys. Same loop, your process, your operational burden.
  • The biggest reliability win isn't picking the "best" mechanism. It's noticing when "always" is masquerading as "when relevant."

If you've been wiring Claude Code config long enough to feel the shape of a coherent harness, where Hooks, Skills, Subagents, and CLAUDE.md all sit in one place and reinforce each other, that's the book I wrote.

👉 Claude Code Mastery

References

Top comments (0)