Claude Code gets a lot of attention for what it produces , refactored functions, generated tests, shell commands that actually work. But almost nobody talks about how it's structured underneath. That's a shame, because the architecture is genuinely interesting and teaches you something real about how autonomous AI agents are built at production scale.

Let's go layer by layer.

The Mental Model: A Harness Around a Brain

The first thing to understand is that Claude Code is not just "an LLM with a terminal." There's a deliberate separation between two things:

┌─────────────────────────────────┐
│         MODEL (LLM Brain)       │
│   Reasoning, generation, plans  │
└────────────────┬────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────┐
│                   HARNESS                       │
│                                                 │
│  ┌─────────────────┐   ┌──────────────────────┐ │
│  │   Agent Loop    │   │   Primitive Tools    │ │
│  │ Think→Act→Obs   │   │  Read · Write · Bash │ │
│  ├─────────────────┤   ├──────────────────────┤ │
│  │  Layered Memory │   │     Permissions      │ │
│  │ Org·Proj·User   │   │  Allow · Ask · Block │ │
│  ├─────────────────┤   ├──────────────────────┤ │
│  │   Sub-Agents    │   │       Hooks          │ │
│  │ Isolated ctx    │   │ Deterministic guards │ │
│  └─────────────────┘   └──────────────────────┘ │
│                 │                               │
│                 ▼                               │
│  ┌──────────────────────────────────────────┐   │
│  │         Local Runtime                    │   │
│  │      Filesystem · Shell · MCP            │   │
│  └──────────────────────────────────────────┘   │
└─────────────────────────────────────────────────┘

The model reasons. The harness acts. This separation is load-bearing , it's what prevents the agent from going off the rails.

The Agent Loop: Think → Act → Observe → Decide

Every action Claude Code takes runs through a cycle. It doesn't just execute a plan — it constantly re-evaluates.

┌──────────┐    ┌─────────┐    ┌──────────────┐    ┌──────────┐
│  THINK   │───>│   ACT   │───>│   OBSERVE    │───>│  DECIDE  │
│ (Plan)   │    │ (Tool)  │    │ (Result/err) │    │ Continue │
└──────────┘    └─────────┘    └──────────────┘    └────┬─────┘
      ▲                                                  │
      └──────────────────────────────────────────────────┘
                        (loop back)

In code terms, you can think of this like:

def agent_loop(task, context, tools, memory):
    while not task.is_complete():
        # Think: what should happen next?
        plan = model.reason(task, context, memory.load())
        
        # Act: execute the chosen tool
        tool_call = plan.next_action()
        result = tools.execute(tool_call)
        
        # Observe: what actually happened?
        context.append(tool_call, result)
        
        # Decide: done, continue, or escalate?
        if result.is_error():
            task.handle_failure(result)
        elif plan.is_satisfied(result):
            task.mark_complete()
        # else: loop continues

The observe step is what separates a real agent from a script. The model doesn't assume its action worked , it reads the output and adjusts.

The Runaway Loop Problem

Here's a failure mode that kills most naive agent implementations:

Think → Act → ERROR → Think → Act → (same error) → Think → Act → ...
                                                              ∞

The agent hallucinates that a fix worked, retries the same approach, gets the same error, and spirals. Sources of this: hallucination, API failures, i/o errors, or just bad output that the model misinterprets as success.

Claude Code handles this at the harness level not by making the model smarter, but by adding guardrails the model can't override:

class AgentLoopGuard:
    MAX_RETRIES = 5
    BACKOFF_STRATEGY = "exponential"
    
    def __init__(self):
        self.attempt_log = []
        self.error_fingerprints = set()
    
    def should_continue(self, last_result):
        # Detect repeated identical errors
        fingerprint = hash(last_result.error_type + last_result.context)
        
        if fingerprint in self.error_fingerprints:
            raise RunawayLoopException("Repeated error detected — stopping agent")
        
        self.error_fingerprints.add(fingerprint)
        
        if len(self.attempt_log) >= self.MAX_RETRIES:
            raise MaxAttemptsException("Loop limit reached")
        
        return True

The fix isn't model-level intelligence. It's deterministic code that the model runs inside.

Layered Memory: Why It Never Starts From Zero

Most agents suffer from what you could call global amnesia every session, they know nothing about the project, the team conventions, the quirks of the codebase. They're perpetually new.

Claude Code solves this with a memory hierarchy:

┌─────────────────────────────────────────────┐
│             MEMORY LAYERS                   │
│                                             │
│  ORG        Global rules, shared context    │
│  ──────────────────────────────────────     │
│  PROJECT    Codebase-specific memory        │
│  ──────────────────────────────────────     │
│  USER       Personal preferences, history   │
│  ──────────────────────────────────────     │
│  AUTO       This session, auto-generated    │
└─────────────────────────────────────────────┘

Each layer persists independently. The AUTO layer is rebuilt every session from recent context. The PROJECT and USER layers survive across sessions. The ORG layer is set once and rarely changes.

# Example: .claude/project_memory.yaml
project:
  name: "payments-service"
  language: "Go"
  conventions:
    - "Always use structured logging (zerolog)"
    - "Table-driven tests only"
    - "No direct DB calls outside repository layer"
  known_gotchas:
    - "config.Load() must be called before any DB init"
    - "Stripe webhook handler is NOT idempotent — do not retry"

When the agent starts, it loads these layers into its context. It knows the codebase before it reads a single file.

Primitive Tools: The Only Hands the Agent Has

The model can reason about anything. But it can only do things through a fixed set of primitives:

┌─────────────────────────────────────────────┐
│              PRIMITIVE TOOLS                │
│                                             │
│  READ      Read files, directories, diffs   │
│  WRITE     Create, modify, delete files     │
│  BASH      Execute shell commands           │
│  WEB       Fetch URLs (when enabled)        │
└─────────────────────────────────────────────┘

This constraint is intentional. The narrower the tool surface, the easier it is to audit, permission, and sandbox. A model that could call arbitrary APIs or spawn processes freely would be nearly impossible to secure.

interface Tool {
  name: string;
  description: string;
  inputSchema: JSONSchema;
  execute(input: unknown): Promise<ToolResult>;
}

// Every tool follows this contract
const readFileTool: Tool = {
  name: "read_file",
  description: "Read contents of a file at given path",
  inputSchema: {
    type: "object",
    properties: {
      path: { type: "string" },
      start_line: { type: "number", optional: true },
      end_line: { type: "number", optional: true }
    }
  },
  execute: async ({ path, start_line, end_line }) => {
    // Permission check happens here, before any I/O
    permissions.assertCanRead(path);
    return filesystem.read(path, start_line, end_line);
  }
};

Permissions: Allow, Ask, Block

The permission model is three-tiered and composable:

┌──────────────────────────────────────────────────────────┐
│                   PERMISSION MODEL                       │
│                                                          │
│  ALLOW   →  Execute without interrupting the user        │
│  ASK     →  Pause, show the action, wait for approval    │
│  BLOCK   →  Never execute, regardless of model intent    │
└──────────────────────────────────────────────────────────┘

This matters because not all actions carry equal risk. Reading a file is low risk allow it. Deleting files outside the project directory? Block it absolutely. Running a migration script? Ask first.

Hooks: Deterministic Guardrails

Hooks are the harness's last line of defense. They're deterministic code that runs around tool execution before and after and the model cannot bypass them.

# Hooks fire deterministically, outside model control
@hook(trigger="before_bash")
def security_check(command: str) -> HookResult:
    BLOCKED_PATTERNS = [
        r"rm\s+-rf\s+/",
        r"curl\s+.*\|\s*bash",
        r"chmod\s+777"
    ]
    for pattern in BLOCKED_PATTERNS:
        if re.search(pattern, command):
            return HookResult.BLOCK(f"Command matches blocked pattern: {pattern}")
    return HookResult.ALLOW()

@hook(trigger="after_write")
def audit_log(path: str, content: str) -> None:
    audit.record(action="file_write", path=path, timestamp=now())

The model sees hooks as invisible. It proposes an action, and the harness either lets it through or stops it no negotiation.

Sub-Agents: Isolated Contexts for Parallel Work

For complex tasks, Claude Code can spawn sub-agents child instances that run in isolated contexts. Each sub-agent has its own context window, its own tool access, its own memory slice.

Main Agent
    │
    ├── Sub-Agent A: "Write unit tests for auth module"
    │       └── isolated context, read-only file access
    │
    ├── Sub-Agent B: "Analyze DB schema for migration risks"
    │       └── isolated context, read-only DB access
    │
    └── [waits for both, synthesizes results]

This solves context collapse the tendency for long tasks to push earlier context out of the window. Splitting work across sub-agents keeps each context focused and fresh.

The Design Pillars, Stated Plainly

After walking through the layers, the design philosophy crystallizes into five ideas:

  1. Model-driven autonomy the LLM decides; the harness executes
  2. Context is scarce every token costs; memory layers compensate
  3. Layered memory agents shouldn't start from zero every session
  4. Declarative extensibility behavior configured, not hardcoded
  5. Composable permissions access controlled at every layer, not just the edges

Why This Architecture Matters Beyond Claude Code

The same pattern applies to any production AI agent: keep the model in charge of reasoning, keep deterministic code in charge of safety. The harness pattern model + loop + tools + memory + permissions + hooks is reusable across almost any agentic system you'd build.

The agents that fail in production almost always violate one of these layers. They let the model call tools without permission checks. They skip memory, so the agent re-learns the same things every run. They have no loop guards, so errors spiral.

Claude Code's architecture isn't magic. It's just careful layering and that's exactly what makes it work.

The same harness pattern applies to any serious agent deployment: the model should be the brain, not the body. Separation of reasoning from execution is what makes autonomous systems safe enough to actually use.

If you found this helpful, you can buy me a beer at: https://buymeacoffee.com/kanishksinn