How Claude Code Works: Inside the Agent Loop
Open a chat with an LLM and ask it to fix a bug, and the best you get back is a suggested diff you copy into your editor by hand — the model never touched your repository, never ran your tests, and forgot the whole exchange the moment you closed the tab. Open Claude Code and give it the same instruction, and it reads the actual files, greps the actual repository, edits code in place, runs the actual test suite, and reports what changed — inside your real working directory, with real side effects. That gap is not a smarter model; the same model can sit behind either interface. It is a different piece of software wrapped around the model, commonly called a harness or agent loop, that gives the model eyes (tools that read), hands (tools that write and execute), and a way to keep going without a human retyping the next instruction after every reply.
This article opens the harness up. We follow one running example — a ticket, PAY-217: refund webhook double-processes when Stripe retries, in a payments-api repository — through the mechanics that make Claude Code an agent rather than a chatbot: the read-plan-act-observe loop, the tool-call system that turns model output into filesystem and shell changes, how context grows and gets compacted as a session runs long, the permission gate that decides what the model is allowed to do without asking, and how subagents, MCP servers, and skills extend the same loop without changing how it fundamentally works. Facts here describe Claude Code's documented architecture as of July 2026 — check current release notes before relying on exact tool names or defaults in production.
A session, not a chat
A plain chat LLM session is a sequence of independent calls: you send text, the model predicts more text, and the only thing that persists between turns is the growing transcript you both keep re-reading. It has no filesystem, no shell, no way to check whether the code it suggested actually compiles. If the answer is wrong, you find out later, manually.
Claude Code wraps the same kind of model in a harness: a program that gives the model a defined set of tools (read a file, edit a file, run a shell command, search a codebase) and a loop that keeps calling the model, executing whatever it asks for, and feeding the result back in, until the model produces a plain-text answer with no further tool requests. The model still only predicts tokens — the harness is what turns some of those tokens into real filesystem writes and real subprocess execution, and what decides which of those writes are allowed to happen without asking you first.
Running example for this article: a Claude Code session in payments-api is asked to fix PAY-217 — a refund webhook that processes the same Stripe refund twice when Stripe retries a failed acknowledgment. We'll carry that ticket through the loop, the tool calls, the context that accumulates while investigating it, the permission check that fires when the agent goes looking for a secret, and a subagent spawned to search for other call sites with the same bug.
| Property | Chat LLM session | Claude Code session |
|---|---|---|
| Filesystem | None — text only | Real reads/writes in the working directory |
| Execution | None — you run suggested code yourself | Runs shell commands via a Bash tool |
| State across turns | Only the visible chat transcript | Transcript + working directory + memory files |
| Stopping point | One reply, always | Loops until no more tool calls or a budget is hit |
Quick reference
- The model is the same kind of component in both cases — the harness around it is what differs.
- "Agent" in this article means specifically: a loop + tool set + permission gate wrapped around an LLM, not a marketing label.
- For how to configure that harness day to day (CLAUDE.md, skills, hooks), see Claude Code Workflow — this article covers the mechanics underneath it.
- For the general vocabulary of agentic systems this pattern belongs to, see What Is Agentic AI?.
Remember this
An agent isn't a smarter model — it's the same model wrapped in a harness that turns some of its output into real filesystem writes and shell execution instead of just displayed text.
The agentic loop
Every turn of the loop starts the same way: the harness assembles a context window from the system prompt, any loaded memory files (CLAUDE.md), and the transcript so far — the user's messages plus every prior tool call and its result. It sends that window to the model and asks for the next step.
The model's response is one of two shapes: plain text, which ends the loop and shows the reply to you, or a tool call — a structured request naming a tool and its input, such as Read with a file path, or Bash with a shell command. There is no separate "planner" module deciding what to do next; the plan is whatever the model reasons through in its own output, sometimes made visible by writing a checklist with a TodoWrite-style tool. When the model emits a tool call, the harness — not the model — executes it, captures the result (file contents, command stdout/stderr, a diff, or an error), and appends that result to the transcript as the next turn's input. Then the loop repeats.
For PAY-217, a typical loop looks like: read the webhook handler → grep for refund across payments-api to find related call sites → read the Stripe SDK's retry documentation the team keeps in /docs → propose an idempotency-key check → edit the handler → run the test suite → read the failure output → adjust → run again → report the fix. Each arrow in that chain is one full turn: model requests a tool, harness executes it, result comes back, model reads the result and decides the next step.
Quick reference
- Tool calls and their results are just more transcript — the model "remembers" what it did only because the record of it is still in the window, not because of separate memory.
- A model with no more useful tool calls to make (task looks done, or it's stuck) stops the loop by answering in plain text — there's no explicit "done" signal beyond that.
- Multi-step plans are emergent from the model's own reasoning in the transcript, not a hardcoded planning phase the harness runs separately.
- A stray or looping tool-call sequence is bounded by turn/token budgets and permission checks, not by the model reliably knowing when to stop on its own.
Remember this
The loop has no separate planning step — what looks like a plan is the model reasoning inside the same transcript that holds every tool call and result, one turn at a time.
The tool-use system: Read, Edit, Bash, and friends
Claude Code ships a fixed catalog of built-in tools: file tools (Read, Write, Edit, Glob, Grep), an execution tool (Bash), web tools (WebFetch, WebSearch), a delegation tool (Task, covered below), and a few smaller utility tools. Each one is described to the model as a name, a short natural-language description, and an input schema — the model conditions on that description the same way it conditions on any other text, which is why vague tool descriptions produce vague, wrong tool calls just as vague prompts produce vague answers.
Bash is the one that matters most for how Claude Code touches your real project: it is a literal shell, running in your actual working directory. Git is not a separate "git tool" with its own logic — git status, git diff, and git commit are just Bash commands the model already knows how to type, which is also why permission rules can restrict individual git subcommands (Bash(git push:*)) without the harness needing any git-specific code. The same is true of test runners, linters, and build tools: the agent's ability to "run your tests" is entirely the model knowing your project's test command and the harness letting Bash execute it.
Before any tool actually runs, the harness checks it against the permission system (next section) — a tool call is a request, not an automatic action. File tools also enforce a smaller rule of their own: Edit typically refuses to modify a file the session hasn't Read yet in that same session, so the model can't blind-edit a file it never actually looked at, even if it remembers the file from an earlier unrelated task.
Quick reference
- Tool descriptions are prompts too — an ambiguous description gets called at the wrong time or with the wrong input, the same failure mode as a vague system prompt.
Bashgives Claude Code its shell and git integration "for free" — there's no special git subsystem, just a model that knows git commands and a shell that can run them.- File tools operate on real paths in the working directory;
Grep/Globreturn matches and file lists, not summaries — the model still has to read the ones that matter. - A tool result that's an error (failed test, denied permission, command not found) is not a crash — it's just the next turn's context, and the model reasons about it like any other observation.
Remember this
Git and test-runner "integration" isn't a separate feature — it's the Bash tool executing commands the model already knows, gated by the same permission check as everything else.
Context management and compaction
Every tool result becomes part of the context window for the rest of the session, whether or not it turns out to matter. Reading the webhook handler is a few hundred tokens; grepping refund across payments-api and reading every match to check relevance can be thousands. On a long PAY-217 investigation — read handler, grep repo, read Stripe docs, run tests three times, read three failure logs — the transcript keeps growing, and a full context window degrades answer quality well before it's technically exhausted, a mechanism covered in more depth in Context Rot in AI Agents.
Claude Code's answer is compaction: when the transcript approaches a threshold of the available context window, the harness summarizes the older portion of the session into a condensed digest — what files were touched, what was decided, what's still open — and replaces the verbose raw tool output with that summary, freeing headroom to keep working. You can also trigger it manually with /compact. This is a different mechanism from the subagent isolation covered next: compaction shrinks one window in place, mid-session; a subagent gets an entirely separate window from the start and never grows the parent's context at all (see Subagents Explained for that mechanism specifically).
Compaction is lossy by construction, which has a practical consequence: a constraint mentioned once early in a long session ("never touch legacy/", "the staging webhook secret rotates weekly") can get summarized away if it wasn't reinforced. That's the mechanism behind advice you'll see elsewhere to put durable rules in CLAUDE.md rather than only stating them mid-chat — CLAUDE.md is reloaded fresh each session rather than surviving as compressible transcript. For the general discipline of deciding what belongs in the window at all, see Context Engineering.
Quick reference
- Compaction condenses the transcript in place — it does not reset the working directory, git state, or any file the agent already edited.
- Recent turns are typically kept close to verbatim; older ones are the ones most likely to be summarized or dropped.
- A constraint stated once mid-session is more compaction-fragile than the same constraint written into CLAUDE.md, which loads fresh every session.
/compactrun manually before a context-heavy step (e.g. before reading a large log) is cheaper than waiting for an automatic compaction mid-task.
Remember this
Compaction summarizes one window in place to buy headroom — it's a different mechanism from a subagent's fresh window, and it can quietly drop a constraint that was never reinforced.
Permission modes and safety confirmations
Every tool call is checked against a permission rule before it executes — matched by tool name and input pattern, such as Bash(git *) (allow) or Read(.env*) (deny). A call that matches neither an allow nor a deny rule falls back to the session's mode: default mode asks before edits and shell commands but lets reads run freely; plan mode blocks all side effects so the model can only read and propose, useful for a first pass before you've seen what it intends to touch; auto-accept edits skips the file-edit prompt for a faster loop while still gating Bash; and a bypass-permissions mode runs everything without asking, meant only for isolated, sandboxed environments where a wrong command can't reach anything you care about.
This matters because the model's own judgment is not the safety boundary — permission rules are enforced by the harness outside the model's reasoning, which is the point. A model can be wrong, or steered by content it read (a prompt-injection risk covered in Jailbreaking vs Prompt Injection), but a deny rule on Read(.env*) blocks the read regardless of what the model concluded it needed. Hooks (PreToolUse) add a further layer: a deterministic script that runs before the tool executes and can block it by exit code, independent of both the model and the static permission rules — see Claude Code Workflow for hook configuration in depth.
Back to PAY-217: while debugging why Stripe's signature check on the retried webhook fails, the agent reasons it needs the Stripe webhook signing secret and requests Read(.env). The rule set denies any .env* read outright. The tool call fails with a permission_denied result — not a crash, just another observation — and the model reads that denial, changes approach, and asks the user to paste the variable name it needs (STRIPE_WEBHOOK_SECRET) rather than its value, so it can grep for where that name is already referenced in code instead of ever seeing the secret.
Quick reference
- Permission checks run outside the model's reasoning — a denial isn't the model being polite, it's the harness refusing to execute regardless of the model's justification.
- Plan mode is the cheapest way to review an agent's intent on unfamiliar code before it can edit or run anything.
- Bypass-permissions mode is appropriate only inside a real sandbox (container, disposable VM) — never on a machine with production credentials reachable.
- A denied tool call becomes a normal observation the model reasons about next, not a terminated session — expect it to try an alternative, not just stop.
Remember this
A permission rule blocks the tool call itself, outside the model's reasoning — that's what makes Read(.env*): deny hold even when the model has a plausible-sounding reason to read it.
Extending the loop: subagents, MCP, and skills
Three mechanisms extend what a Claude Code session can do, and it's worth keeping them separate because they solve different problems. A subagent, spawned through the Task tool, is a second agent invocation with its own fresh context window and its own tool-call budget, given one bounded task and returning a compressed result rather than a transcript. For PAY-217, once the fix works in the webhook handler, the parent can spawn a subagent to fan out across payments-api searching for every other place that processes a Stripe event without an idempotency check — the search noise (dozens of files, hundreds of lines) stays in the subagent's disposable window; only a short list of file paths and line numbers crosses back. The full mechanism, including the tool-budget cutoff and why fan-out beats sequential search for independent slices, is covered in Subagents Explained.
MCP (Model Context Protocol) solves a different problem: giving the agent tools it doesn't ship with. Claude Code acts as an MCP host, connecting to MCP servers — local processes or remote endpoints — that expose additional tools, resources, or prompts, registered in .mcp.json or via claude mcp add. A Stripe MCP server, for example, could expose a list_recent_events tool that queries Stripe's API directly instead of the agent trying to reconstruct that from webhook logs. Once connected, an MCP tool behaves exactly like a built-in one inside the loop from section two — same request/permission/execute/observe cycle, just a larger tool catalog. See MCP vs A2A vs ACP for how MCP compares to adjacent agent protocols.
Skills solve a third, unrelated problem: packaging procedural knowledge, not adding capability. A skill is a markdown file with a description Claude matches against natural language to auto-invoke — "how this team writes commit messages," "how to run the payments-api migration checklist" — and it steers how existing tools get used rather than granting any new one. Confusing a skill with an MCP server is a common mistake: a skill can't call an external API on its own; it can only tell the model, in prose, how to use the tools it already has. See Claude Code Workflow for how to author one.
Quick reference
- Subagent = capacity management (protect the parent's context, parallelize search); MCP = capability expansion (new tools); skill = procedural guidance (steer existing tools).
- An MCP tool joins the same tool-call loop as
ReadorBash— connecting a server doesn't change the loop's mechanics, only its tool catalog. - A subagent cannot message a sibling subagent or see the parent's other in-flight work — it is closer to a function call than a coworker.
- A skill with a vague description simply never auto-activates — the description field is the entire matching mechanism, same as a tool description.
Remember this
Subagents manage context capacity, MCP servers add tool capability, and skills add procedural knowledge — three different problems that happen to share the same underlying tool-call loop.
Key takeaway
None of this requires a smarter model than the one behind a plain chat window — it requires a harness that turns tool calls into real filesystem writes and shell execution, a permission gate that enforces boundaries outside the model's own judgment, and a compaction mechanism that keeps a long session from drowning in its own tool output. PAY-217 moved from ticket to tested fix through exactly that loop: read, call, observe, repeat, with a denied .env read and a fan-out subagent along the way.
Practice (20 min): In a real or scratch repository with Claude Code installed, ask it to find and fix one small bug, and watch the transcript for each tool call and result rather than just the final answer. Expected result: a sequence of Read/Grep/Edit/Bash calls ending in a passing test run. Intentionally break it by asking the agent, mid-task, to read a file matching a deny rule you set in .claude/settings.json (e.g. Read(.env*)) — confirm the call is refused rather than silently skipped. Recover by asking the agent to proceed using only variable names you supply, not file contents. Pass when the original bug fix completes, the denied read shows up explicitly as a refused tool call in the transcript, and a fresh session in the same repo reaches the same result without you re-explaining the deny rule.
Related Articles
Explore this topic