How Claude Code
Actually Works

Building an AI coding agent from first principles

$ python v0_bash_agent_mini.py
"It's just a while loop."

The Universal Pattern

while True:
    response = model(messages, tools)
    if response.stop_reason != "tool_use":
        return response.text
    results = execute(response.tool_calls)
    messages.append(results)

Every agent you've ever used is a variation of these 6 lines.

User Model Tool Call Execute result text response (loop exits)

DEMO: The Agent Loop in Action

Scenario: User asks "What files are in this project?"

messages[ ]
terminal output

v0 — Bash is All You Need

A complete AI agent in ~100 lines. Single tool: bash

# The ONE tool that does everything
T = [{
    "name": "bash",
    "description": "Shell cmd. Read:cat/grep. Write:echo>/sed.
        Subagent: python v0_bash_agent_mini.py 'task'",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]

# The agent loop
def chat(prompt, history=[]):
    history.append({"role": "user", "content": prompt})
    while (r := client.messages.create(
        model=M, system=S, messages=history, tools=T, max_tokens=8000
    )).stop_reason == "tool_use":
        # ... execute tools, append results ...
    return "..."

The tool description IS the prompt engineering. It teaches the model how to use bash and how to spawn subagents.

Subagents for Free

Process isolation = Context isolation

  • bash tool spawns another instance of itself:
    python v0_bash_agent_mini.py 'subtask'
  • Each subprocess gets its own clean messages = []
  • No extra code needed — Unix process model gives this for free
  • Recursive calls enable unlimited nesting
# Parent agent
Main Agent
  |-- bash: python v0_bash_agent.py
       "analyze architecture"
       |
       |-- Subagent (fresh history=[])
            |-- bash: find . -name "*.py"
            |-- bash: cat src/main.py
            |-- Returns summary via stdout

v1 — Dedicated Tools

Core insight: "Model as Agent" — 4 essential tools cover 90% of coding tasks

ToolPurpose
bashRun any command
read_fileRead file contents
write_fileCreate/overwrite files
edit_fileSurgical text replacement

Why not just bash? Dedicated tools give:

  • Structured I/O the model can reason about
  • Safety checks (path validation, cmd blocking)
  • Precise edits (exact string match vs sed regex)
# The agent loop core (v1)
def agent_loop(messages):
    while True:
        response = client.messages.create(
            model=MODEL, system=SYSTEM,
            messages=messages,
            tools=TOOLS, max_tokens=8000
        )

        if response.stop_reason != "tool_use":
            return messages

        for tc in tool_calls:
            output = execute_tool(tc.name, tc.input)
            results.append({...})

        messages.append(results)

Tool Design Principles

What makes a good tool?

  • Atomic: Does one thing well
  • Well-described: The model reads descriptions to decide what to use
  • Composable: Tools work together through the message history

Anti-pattern: Giant Swiss-army-knife tools with 20 parameters

Principle: Start with 3-5 atomic tools, add more only when the agent consistently fails

# Actual tool schema from v1
{
  "name": "edit_file",
  "description": "Replace exact text
    in a file. Use for surgical edits.",
  "input_schema": {
    "type": "object",
    "properties": {
      "path":     {"type": "string"},
      "old_text": {"type": "string",
        "description": "Exact text
          to find (must match)"},
      "new_text": {"type": "string"}
    },
    "required": ["path",
      "old_text", "new_text"]
  }
}

Prompt Engineering for Tools

The description IS the prompt

  • The model reads tool descriptions to decide: which tool, what arguments, what order
  • Minimal system prompts work — 6 lines in v1
  • Quality of tool descriptions directly affects agent behavior
# The complete v1 system prompt
SYSTEM = f"""You are a coding agent at {WORKDIR}.

Loop: think briefly -> use tools
      -> report results.

Rules:
- Prefer tools over prose.
- Never invent file paths.
- Make minimal changes.
- After finishing, summarize."""

Vague vs Precise

# Bad: vague description
"description": "Run a command"

# Good: teaches the model patterns
"description": """Execute shell
command. Common patterns:
- Read: cat/head/tail, grep/find
- Write: echo 'x' > file, sed
- Subagent: python v0.py 'task'
  (spawns isolated agent)"""

The precise description teaches the model how to use the tool effectively. The vague one leaves the model guessing.

DEMO: Multi-Tool Agent

Scenario: "Fix the typo in README.md where it says 'teh' instead of 'the'"

messages[ ]
agent output

Key insight: The model chose to read first, then edit. Nobody told it to. The loop + tools + model intelligence = emergent agent behavior.

v2 — Structured Planning

Core insight: "Make Plans Explicit"

  • Problem: Complex tasks need structure; the model loses track
  • Solution: TodoManager tool — the agent manages its own task list
  • Key constraints that help:
    • Max 20 items
    • Only 1 in_progress at a time
    • Required fields: content, status, activeForm

"Constraints aren't limitations — they're scaffolding"

class TodoManager:
    def update(self, items):
        # Validate each item
        for item in items:
            if status == "in_progress":
                in_progress += 1

        # Enforce constraints
        if len(validated) > 20:
            raise ValueError("Max 20")
        if in_progress > 1:
            raise ValueError("Only one")

        self.items = validated
        return self.render()

    def render(self):
        # [x] Completed  [>] Working  [ ] Pending
        ...

DEMO: Planning in Action

Scenario: "Add a /health endpoint to the API with tests"

agent actions

Todo List

v3 — Subagents

Core insight: "Divide and Conquer with Context Isolation"

  • Problem: Long conversations accumulate noise (context pollution)
  • Solution: Task tool spawns child agents with fresh, clean context
  • Agent types with different trust levels:
TypeToolsPurpose
explorebash, read_fileRead-only research
codeall toolsFull implementation
planbash, read_fileDesign/architecture
def run_task(desc, prompt, agent_type):
    config = AGENT_TYPES[agent_type]

    # ISOLATED context - the key!
    sub_messages = [
        {"role": "user", "content": prompt}
    ]

    # Filtered tools for this agent type
    sub_tools = get_tools_for_agent(agent_type)

    # Same agent loop, different context
    while True:
        response = client.messages.create(
            model=MODEL, system=sub_system,
            messages=sub_messages,
            tools=sub_tools, max_tokens=8000
        )
        if response.stop_reason != "tool_use":
            break
        # ... execute tools ...

    # Return ONLY the final summary
    return block.text

Context is Everything

The hidden art of AI engineering

  • Context window = working memory (finite!)
  • Too much context = confused model, slow responses, high cost
  • Strategies: subagent isolation, output truncation, selective tool availability
  • This is why Claude Code feels "smart" — better organized context, not a smarter model
Without Subagents user: "Refactor auth" cat auth.py (500 lines) cat models.py (300 lines) cat routes.py (400 lines) FULL (confused) "Wait, what was I doing?" With Subagents user: "Refactor auth" Task(explore) -> summary Task(plan) -> strategy Task(code) -> changes ~ ~ CLEAN (focused) Only summaries retained

v4 — Skills & Knowledge

Core insight: "Knowledge Externalization"

  • Tools = what the agent CAN do (capabilities)
  • Skills = what the agent KNOWS (knowledge)
  • SKILL.md files: YAML frontmatter + Markdown body
  • Loaded on-demand, not baked into the model
ApproachCostTimeline
Fine-tuning$1K–$1M+Weeks
SKILL.md fileFreeMinutes

Like hot-swappable LoRA adapters without any training!

class SkillLoader:
    def load_skills(self):
        # Scan skills/ directory
        for skill_dir in self.skills_dir.iterdir():
            skill_md = skill_dir / "SKILL.md"
            if skill_md.exists():
                skill = self.parse_skill_md(skill_md)
                self.skills[skill["name"]] = skill

    def get_skill_content(self, name):
        # Layer 2: full body on-demand
        skill = self.skills[name]
        content = skill["body"]

        # Layer 3: list available resources
        for folder in ["scripts", "references"]:
            ...

        return content

SKILL.md Deep Dive

---
name: code-review
description: Perform thorough code reviews
  with security, performance, and
  maintainability analysis.
---

# Code Review Skill

## Review Checklist

### 1. Security (Critical)
- [ ] Injection vulnerabilities
- [ ] Authentication issues
- [ ] Authorization flaws
- [ ] Data exposure

### 2. Correctness
- [ ] Logic errors, off-by-one
- [ ] Race conditions
- [ ] Resource leaks

### 3. Performance
- [ ] N+1 queries
- [ ] Blocking operations
- [ ] Missing caches

Progressive Disclosure

Layer 1: Metadata ~100 tokens
Name + description (always loaded)
Layer 2: SKILL.md body ~2000 tokens
Detailed instructions (on trigger)
Layer 3: Resources unlimited
Scripts, references, assets (as needed)

Cache-Preserving Injection

Skills go in tool_result messages, not system prompt. System prompt unchanged = prompt cache hit. Saves ~90% on repeated content.

Safety & Security Patterns

Building agents you can trust

# Path traversal prevention
def safe_path(p: str) -> Path:
    path = (WORKDIR / p).resolve()
    if not path.is_relative_to(WORKDIR):
        raise ValueError(f"Path escapes workspace: {p}")
    return path

# Dangerous command blocking
dangerous = ["rm -rf /", "sudo", "shutdown",
             "reboot", "> /dev/"]
if any(d in command for d in dangerous):
    return "Error: Dangerous command blocked"

# Output truncation (prevent context overflow)
return output[:50000]

# Timeout enforcement
subprocess.run(cmd, timeout=60)

Defense in Depth

  • safe_path() — prevents ../ escapes
  • Dangerous command blocking — blocks rm -rf, sudo, etc.
  • Output truncation (50KB) — prevents context overflow attacks
  • Timeout (60s) — prevents hanging

Agent Type Restrictions (v3)

  • explore = read-only (can't write)
  • plan = read-only (can't modify)
  • code = full access (for implementation)

Production Claude Code adds: permission system, hooks, sandboxing

The Messages API

What's actually happening under the hood

# REQUEST
POST /v1/messages

{
  "model": "claude-sonnet-4-5-20250929",
  "system": "You are a coding agent...",
  "messages": [
    {"role": "user",
     "content": "Fix the bug in auth.py"}
  ],
  "tools": [
    {"name": "bash", ...},
    {"name": "read_file", ...},
    {"name": "edit_file", ...}
  ],
  "max_tokens": 8000
}
# RESPONSE

{
  "content": [
    {"type": "text",
     "text": "Let me read the file..."},
    {"type": "tool_use",
     "name": "read_file",
     "input": {"path": "auth.py"}}
  ],
  "stop_reason": "tool_use"
}

Every "AI agent" is:

  1. Send JSON
  2. Get JSON back
  3. Parse & execute tools
  4. Send JSON again

The "magic" is in the model's responses, not the API.

Token Economics

Why every design decision exists

Claude Sonnet Pricing
Input tokens~$3 / million
Output tokens~$15 / million
200K context at capacity~$0.60 per call
Output tokens cost5x input

This drives EVERYTHING:

  • Why truncate output to 50KB?
    Cost + context pollution
  • Why subagents?
    Isolated context = smaller, cheaper calls + better reasoning
  • Why skills in tool_result?
    Prompt caching saves ~90% on repeated content
  • Why output limits?
    Output tokens cost 5x input tokens

The art: minimize tokens while maximizing useful context.

Provider Abstraction

One agent, many models

class UnifiedAPIClient:
    def __init__(self, provider=None):
        self.provider = provider or \
            os.getenv("API_PROVIDER", "anthropic")

        if self.provider == "anthropic":
            self.client = Anthropic(...)
            self.default_model = "claude-sonnet-4-5-..."
        elif self.provider == "openai":
            self.client = OpenAI(...)
            self.default_model = "gpt-4"

Swap providers with one env var:
API_PROVIDER=openai

What Gets Translated

AspectTranslation
Tool schemasAnthropic ↔ OpenAI functions
Messagescontent blocks ↔ tool_calls
Stop reasons"tool_use" ↔ "tool_calls"
System promptseparate param ↔ in messages

Why it matters: avoid vendor lock-in, test across models, use the best model for each task.

MCP — The Plugin System

Model Context Protocol: how agents connect to the world

  • Problem: Every tool is hardcoded. How do you add new capabilities?
  • MCP: Standardized protocol for tools, resources, and prompts
  • Anyone can write an MCP server — it's how the ecosystem grows
Agent ↔ MCP Server ↔ External Service
(database, API, browser, etc.)
# Python MCP Server (minimal)
from mcp.server import Server

server = Server("my-server")

@server.tool()
async def hello(name: str) -> str:
    """Say hello."""
    return f"Hello, {name}!"

# Register in ~/.claude/mcp.json
{
  "mcpServers": {
    "my-server": {
      "command": "python3",
      "args": ["/path/to/server.py"]
    }
  }
}

Architecture Recap

The full picture, layer by layer

v0: Loop + Bash the foundation
v1: + Dedicated Tools precision & safety
v2: + Todo Manager planning & structure
v3: + Subagents scale & isolation
v4: + Skills knowledge & expertise

Each version builds on the previous while maintaining the core agent loop pattern.

How This Maps to Production Claude Code

Learn RepoProduction Claude Code
bash, read_file, write_file, edit_file Read, Write, Edit, Bash, Glob, Grep
TodoManager TodoWrite / TaskCreate
Task (subagents) Task tool (Explore, Plan, Code agents)
SKILL.md files Skills (/commit, /review-pr, etc.)
api_client.py Multi-provider support
safe_path, cmd blocking Permission system, hooks, sandboxing
— + MCP servers, context compression, streaming

The patterns are identical. Production adds scale, polish, and security — but the architecture is the same.

Why Constraints Matter

Structure constrains AND enables

  • max_tokens forces conciseness
  • Tool schemas force structured output
  • Todo limits force prioritization
  • Agent type restrictions force safety
  • Output truncation forces focus

Without constraints: agents ramble, lose focus, waste tokens

Analogy

Poetic forms (sonnets, haiku) produce better poetry than "write anything".

Agent constraints produce better agents than "do anything".

Key Takeaways

Try It Yourself

github.com/anthropics/learn-claude-code

1. python v0_bash_agent_mini.py
2. Progress through v0 → v4
3. Write your own SKILL.md
4. Build an MCP server
5. Build an agent for your domain
$ git clone && python v0_bash_agent_mini.py