Back

Context, Skills, Hooks, Subagents: How Claude Code Actually Works

Jumpspace·

The thing most people get wrong early: they treat Claude Code like a smarter chatbot. It isn’t. The failures that accumulate over time, drifting context, degrading output quality, and rules that get ignored aren’t model failures. They’re architecture failures. Fix the architecture, and the model mostly takes care of itself.

Here’s how I think about it now.

The Six-Layer Model

Every Claude Code setup has six layers, whether you design them intentionally or not:

  1. Context (what Claude actually sees)

  2. Skills (workflow packages loaded on demand)

  3. Tools and Model Context Protocol servers (available actions)

  4. Hooks (deterministic scripts attached to Claude’s actions)

  5. Subagents (isolated Claude instances for contained work)

  6. Verification (how you confirm something worked)

Neglect any layer, and it creates pressure somewhere else. A bloated CLAUDE.md pollutes the context. Too many tools create selection noise. Skipping verification means you’re flying blind. The layers are load-bearing.

How the Agent Loop Actually Works

Gather context → Take action → Verify result → [Done or loop back]
     ↑                    ↓
  CLAUDE.md          Hooks / Permissions / Sandbox
  Skills             Tools / MCP
  Memory

Wrong information in context causes more damage than missing information. The model will confidently act on bad inputs. And without a verification step, you often won’t know something went sideways until several steps later, when untangling it is expensive.

When things break: check context loading order before blaming the model. Check your control layer before calling the agent “too aggressive.” If a long session is producing worse results, don’t tune prompts — start fresh. Stale intermediate artefacts accumulate and degrade output in ways that are hard to diagnose.

Context: The Real Cost Structure

The 200K context window sounds generous until you account for what’s already eating it:

200K total context
├── Fixed overhead (~15-20K)
│   ├── System instructions: ~2K
│   ├── All enabled Skill descriptors: ~1-5K
│   ├── MCP Server tool definitions: ~10-20K  ← Largest hidden overhead
│   └── LSP state: ~2-5K
│
├── Semi-fixed (~5-10K)
│   ├── CLAUDE.md: ~2-5K
│   └── Memory: ~1-2K
│
└── Dynamically available (~160-180K)
    ├── Conversation history
    ├── File contents
    └── Tool call results

A single Model Context Protocol server like GitHub can expose 20–30 tool definitions at ~200 tokens each. Five servers connected at once and you’ve spent ~25,000 tokens (12.5% of total) before a single message is sent. In large codebases, this is a real constraint.

The layering that works:

Always resident    → CLAUDE.md: project contract / build commands / prohibitions
Path-loaded        → rules: language / directory / file-type specific
On-demand loaded   → skills: workflows / domain knowledge
Isolated loaded    → subagents: heavy exploration / parallel research
Never in context   → hooks: deterministic scripts / audit / blocking

On compression: The default algorithm prioritizes re-readability and tends to drop early tool outputs and file contents first. Those often contain architectural decisions you made two hours ago. They disappear quietly and then Claude makes choices that contradict them.

Fix this with explicit Compact Instructions in CLAUDE.md:

## Compact Instructions
When compressing, preserve in priority order:1. Architecture decisions (NEVER summarize)
2. Modified files and their key changes
3. Current verification status (pass/fail)
4. Open TODOs and rollback notes
5. Tool outputs (can delete, keep pass/fail only)

Better still: before closing a session, have Claude write a HANDOFF.md.

Write the current progress in HANDOFF.md. Explain what you tried, what worked, what didn't, so the next agent with fresh context can continue from just this file.

The next session starts from that file instead of depending on compression quality. More reliable than any prompt trick.

Plan Mode: Double-tap Shift+Tab. The exploration phase stays read-only and nothing gets written until you confirm the plan. For refactors and cross-module changes, this one habit prevents a class of errors where a wrong assumption gets baked into execution before you can catch it. A useful pattern: one agent drafts the plan, another reviews it before anything runs.

Skills: Workflows, Not Templates

A skill isn’t a saved prompt. The descriptor stays resident in context; the full body only loads when the skill is actually invoked. That distinction matters for how you design them.

Good skill descriptions specify when to use the skill, not just what’s in it. They define inputs, outputs, and stop conditions. Reference material lives in supporting files, not in SKILL.md itself. Any skill with side effects should explicitly block model auto-invocation.

Stable structure:

.claude/skills/
└── incident-triage/
    ├── SKILL.md
    ├── runbook.md
    ├── examples.md
    └── scripts/
        └── collect-context.sh

Three types worth having:

Quality gate — runs before a release, blocks on failure:

---
name: release-check
description: Use before cutting a release to verify build, version, and smoke test.
---
## Pre-flight (All must pass)
- [ ] `cargo build --release` passes
- [ ] `cargo clippy -- -D warnings` clean
- [ ] Version bumped in Cargo.toml
- [ ] CHANGELOG updated
- [ ] smoke test passes on clean env## Output
Pass / Fail per item. Any Fail must be fixed before release.

Workflow with rollback — high-risk operations, explicit invocation only:

name: config-migration
description: Migrate config schema. Run only when explicitly requested.
disable-model-invocation: true
---
## Steps
1. Backup: `cp ~/.config/app/config.toml ~/.config/app/config.toml.bak`
2. Dry run: `app config migrate --dry-run`
3. Apply: remove `--dry-run` after confirming output
4. Verify: all checks pass## Rollback
`cp ~/.config/app/config.toml.bak ~/.config/app/config.toml`

Decision framework — structured evidence collection instead of guessing:

---
name: runtime-diagnosis
description: Use when the app crashes, hangs, or behaves unexpectedly at runtime.
---
## Evidence Collection
1. Run health check and capture full output
2. Last 50 lines of application logs
3. Plugin or extension state## Decision Matrix
| Symptom | First Check |
|---|---|
| Crash on startup | health output → syntax error |
| Rendering glitch | GPU backend / terminal capability |
| Config not applied | Config path + schema version |## Output Format
Root cause / Blast radius / Fix steps / Verification command

Descriptor length matters. Every enabled skill keeps its descriptor in context permanently.

# Inefficient (~45 tokens)
description: |
  This skill helps you review code changes in Rust projects.
  It checks for common issues like unsafe code, error handling...
  Use this when you want to ensure code quality before merging.
# Efficient (~9 tokens)
description: Use for PR reviews with focus on correctness.

Invocation strategy based on frequency:

  • More than once per session: keep auto-invoke, optimize the descriptor

  • Less than once per session: disable auto-invoke, trigger manually

  • Less than once per month: remove it entirely, document in AGENTS.md instead

Tool Design: Optimize for Selection, Not Coverage

Human-facing Application Programming Interfaces optimize for completeness. Agent-facing tools need to optimize for correct selection. Those are different design goals.

Practical rules: prefix names by system layer (github_pr_*, jira_issue_*), support response_format: concise / detailed, make error messages corrective rather than opaque, and avoid list_all_* patterns that push filtering work onto the model.

How Anthropic learned this the hard way with questioning:

Version 1 — add a question parameter to existing tools like Bash. Claude ignored it and kept going.

Version 2 — require a specific markdown format that triggers a pause. No hard enforcement, so it remained fragile.

Version 3 — a standalone AskUserQuestion tool. The tool call itself is the pause signal. Structurally impossible to skip.

If you want Claude to stop and ask, the only reliable approach is a dedicated tool. Output format conventions are too easy to skip.

On search tools: Early Claude Code relied on a retrieval-augmented generation-style vector database. Fast, but required indexing and broke across environments. Worse, model adoption was poor. Switching to a grep-style tool where Claude searches directly worked better, and had a useful side effect: Claude can read a skill, follow file references, and load information progressively as needed. Progressive disclosure working correctly.

Don’t add a tool if the shell can already handle the task reliably, if the model needs static knowledge rather than external interaction, or if you haven’t validated that the description, schema, and return format are stable for model use.

Hooks: Move Decisions Out of the Model

The right mental model for hooks isn’t “scripts that run automatically.” It’s “things that should never depend on the model remembering to do them.”

Formatting, protected file checks, post-task notifications — these don’t require judgment. They require consistency. Hooks provide that.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "pattern": "*.rs",
        "hooks": [
          {
            "type": "command",
            "command": "cargo check 2>&1 | head -30",
            "statusMessage": "Running cargo check..."
          }
        ]
      }
    ],
    "Notification": [
      {
        "type": "command",
        "command": "osascript -e 'display notification \"Task completed\" with title \"Claude Code\"'"
      }
    ]
  }
}

For mixed-language projects, trigger separately by file type:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "pattern": "*.rs",
        "hooks": [{
          "type": "command",
          "command": "cargo check 2>&1 | head -30",
          "statusMessage": "Checking Rust..."
        }]
      },
      {
        "matcher": "Edit",
        "pattern": "*.lua",
        "hooks": [{
          "type": "command",
          "command": "luajit -b $FILE /dev/null 2>&1 | head -10",
          "statusMessage": "Checking Lua syntax..."
        }]
      }
    ]
  }
}

Finding a compile error on edit 3 is much cheaper than finding it on edit 40. In a 100-edit session, 30–60 seconds saved per edit is 1–2 hours total. Keep output short (| head -30) so hook results don't pollute context.

Hooks aren’t a complete solution on their own. The stack that actually holds:

  • CLAUDE.md: state the rule (“must pass tests and lint before commit”)

  • Skill: define how to execute it (test order, failure handling, fix steps)

  • Hook: enforce it deterministically on critical paths

Each layer alone has gaps. Together they close them.

Don’t use hooks for: complex semantic judgments, long-running business logic, anything requiring multi-step reasoning. That’s what skills and subagents are for.

Subagents: Isolation Over Parallelism

A subagent is an independent Claude instance with its own context window and a restricted tool set. The value isn’t running things in parallel — it’s keeping heavy intermediate output out of your main context.

Codebase scans and test runs that generate thousands of tokens of output? Send them to a subagent. The main thread gets a summary. The garbage stays isolated.

Built-in options: Explore (read-only, runs Haiku to save cost), Plan, General-purpose, and custom.

Configuration constraints worth enforcing:

  • tools / disallowedTools: never give a subagent the same broad permissions as the main thread

  • model: Haiku or Sonnet for exploration, Opus for reviews that matter

  • maxTurns: always set a ceiling

  • isolation: worktree: use this when the subagent needs to touch files

For long-running bash work, Ctrl+B moves it to the background. Claude checks results later with BashOutput without blocking anything.

What subagents are bad at: tasks with strong dependencies between subtasks that require frequent sharing of intermediate state. That kind of tight coupling defeats the purpose.

Prompt Caching: It Shapes Architecture

This gets less attention than it deserves. Cache hit rate directly affects cost, latency, and rate limits. It should inform how you structure everything above it.

Caching works by prefix matching — everything from the start of the request to each cache_control breakpoint gets cached. Claude Code's prompt order:

1. System Prompt → Static, locked
2. Tool Definitions → Static, locked
3. Chat History → Dynamic, comes after
4. Current user input → Last

Common ways people accidentally break caching: putting timestamps in the system prompt, shuffling tool definition order non-deterministically, adding or removing tools mid-session. Dynamic content like the current time belongs in a later message — not the system prompt.

Don’t switch models mid-session. Cache is model-specific. If you’ve accumulated 100K tokens of conversation with Opus and switch to Haiku for a quick question, you pay to rebuild the entire cache for Haiku. It’s more expensive than staying on Opus. If you need to switch, do it via subagent handoff: Opus prepares a summary message, and the new model starts from that.

How compaction actually works: Claude Code forks a summarisation call over the existing conversation (which itself benefits from caching), compresses the turns into a shorter summary, and preserves the system prompt, tool definitions, and referenced files. The session continues with freed space.

Plan Mode doesn’t change the underlying tool prefix. That’s intentional — switching to a read-only toolset would break cache reuse every time you enter and exit planning. The same prefix stays in place.

defer_loading: Rather than including every full tool definition in every request, Claude Code keeps lightweight stubs in the stable prefix and loads full schemas only when a tool is selected. Avoids both the cost of full definitions and the cache instability of adding and removing tools dynamically.

Verification: Define Done Before You Start

The question isn’t whether Claude finished. It’s whether what it produced is correct, and whether you can roll it back if it isn’t.

Verifier levels:

  • Lowest: command exit codes, lint, typecheck, unit tests

  • Middle: integration tests, screenshot comparison, contract tests, smoke tests

  • Higher: production log verification, monitoring metrics, manual review checklists

Define acceptance criteria explicitly in your prompt, in the relevant skill, and in CLAUDE.md:

## Verification
For backend changes:- Run `make test` and `make lint`
- For API changes, update contract tests under `tests/contracts/`For UI changes:- Capture before/after screenshots if visualDefinition of done:- All tests pass
- Lint passes
- No TODO left behind unless explicitly tracked

My test for whether a task is ready for autonomous execution: can I clearly describe what a correct result looks like before Claude starts? If not, the task isn’t ready. A capable model with no acceptance criteria still has no reliable way to know when it’s done.

Commands to Use Constantly

The goal is active context management. Don’t wait for the system to handle it.

Context:

/context   # Inspect token consumption, including MCP and file-read ratios
/clear     # Reset the session; useful when the same issue has already been corrected twice
/compact   # Compress while retaining key points; works best with Compact Instructions
/memory    # Confirm which CLAUDE.md actually got loaded

Governance:

/mcp           # Manage MCP connections, check token costs, disconnect idle servers
/hooks         # Manage hooks; this is a key control-plane entry point
/permissions   # View or update permission whitelist
/sandbox       # Configure sandbox isolation, essential for high-automation scenarios
/model         # Switch models: Opus for deep reasoning, Sonnet for routine, Haiku for quick exploration

Session control:

claude --continue               # Resume the latest session in the current directory
claude --resume                 # Open selector to resume historical session
claude --continue --fork        # Fork from an existing session to try a different approach
claude --worktree               # Create isolated git worktree
claude -p "prompt"              # Non-interactive mode for CI, pre-commit, or other scripts
claude -p --output-format json  # Structured output that scripts can consume directly

A few less obvious ones worth knowing:

/simplify — quick pass over recently modified code, focused on reuse and efficiency. Run it right after changing logic, not as an afterthought.

/rewind — returns to an earlier checkpoint and re-summarises from there. Not an undo. Useful when Claude went deep down the wrong path, and you want to keep the early work but discard what came after.

/btw — side question without interrupting the main task. Fine for quick comparisons. Not for anything that requires reading files or making tool calls.

claude -p --output-format stream-json — real-time JSON event stream. Useful for monitoring long-running tasks or piping output into your own tooling.

/insight — asks Claude to analyse the current session and surface what should be added to CLAUDE.md. Run it after a productive stretch. It catches patterns you'd otherwise miss.

Double-tap Escape brings back your previous input for editing. Faster than restarting when your last message was underspecified.

Session history is local. Everything lives under ~/.claude/projects/ as .jsonl files, organised by project path. To find prior work: grep -rl "keyword" ~/.claude/projects/.

CLAUDE.md: A Contract, Not Documentation

Every entry in CLAUDE.md should answer one question: Does Claude need this in every single session? If not, it belongs somewhere else.

Start empty. Add entries only when you catch yourself repeating the same instruction. Use # to append the current conversation directly, or tell Claude, "add this to the project's CLAUDE.md."

Include:

  • Build, test, lint, run commands

  • Directory structure and module boundaries

  • Code style and naming constraints that aren’t obvious from the repo

  • Environment dependencies and known pitfalls

  • Hard prohibitions (the NEVER list)

  • Compact Instructions

Don’t include:

  • Background context or introductions

  • Full Application Programming Interface documentation

  • Principles like “write clean code” that Claude can’t act on concretely

  • Anything Claude can infer from reading the codebase

  • Reference material for infrequent tasks (put that in skills)

Template that holds up in practice:

# Project Contract
## Build And Test- Install: `pnpm install`
- Dev: `pnpm dev`
- Test: `pnpm test`
- Typecheck: `pnpm typecheck`
- Lint: `pnpm lint`## Architecture Boundaries- HTTP handlers live in `src/http/handlers/`
- Domain logic lives in `src/domain/`
- Do not put persistence logic in handlers
- Shared types live in `src/contracts/`## Coding Conventions- Prefer pure functions in domain layer
- Do not introduce new global state without explicit justification
- Reuse existing error types from `src/errors/`## Safety Rails### NEVER- Modify `.env`, lockfiles, or CI secrets without explicit approval
- Remove feature flags without searching all call sites
- Commit without running tests### ALWAYS- Show diff before committing
- Update CHANGELOG for user-facing changes## Verification- Backend changes: `make test` + `make lint`
- API changes: update contract tests under `tests/contracts/`
- UI changes: capture before/after screenshots## Compact InstructionsPreserve:1. Architecture decisions (NEVER summarize)
2. Modified files and key changes
3. Current verification status (pass/fail commands)
4. Open risks, TODOs, rollback notes

After fixing a mistake, tell Claude: “Update your CLAUDE.md so you don’t make that mistake again.” It’s reasonably good at writing its own rules. Review the file occasionally anyway — stale constraints add noise without adding value.

Project Layout Reference

Project/
├── CLAUDE.md
├── .claude/
│   ├── rules/
│   │   ├── core.md
│   │   ├── config.md
│   │   └── release.md
│   ├── skills/
│   │   ├── runtime-diagnosis/     # Uniformly collect logs, state, dependencies
│   │   ├── config-migration/      # Config migration rollback protection
│   │   ├── release-check/         # Pre-release validation, smoke test
│   │   └── incident-triage/       # Production incident triage
│   ├── agents/
│   │   ├── reviewer.md
│   │   └── explorer.md
│   └── settings.json
└── docs/
    └── ai/
        ├── architecture.md
        └── release-runbook.md

Global constraints in CLAUDE.md, path-specific rules in rules/, task workflows in skills/, architectural detail in docs/ai/. Keep personal defaults in ~/.claude/ and project-specific config in each project's .claude/. Cross-project pollution is real.

The Actual Shift

Most people hit a wall with Claude Code at some point. Output quality drops. Rules get ignored. Sessions drift. The instinct is to fix the prompts.

The prompts usually aren’t the problem.

The constraint that actually matters: before handing a task to Claude for autonomous execution, you need a clear definition of done. Which commands must pass, what the output should look like, and how you’ll verify it. Without that, there’s no signal for Claude or for you that the work is actually finished.

Subscribe to Updates

Get notified when new posts are published.