Claude Code is one of the most-used coding agents in production, and almost everything about how it works is documented in public. In this lesson, we’ll open it up and trace the machine underneath: the loop that turns a text model into an agent, how it holds context and memory, how thinking and tool calls are billed, how hooks and skills and permissions bend its behavior, how it runs work in the background and on remote machines, and how it verifies its own output by running tests and looking at screenshots.
We’ll stay grounded in Anthropic’s own docs and engineering posts, and we’ll draw each mechanism as a diagram, then show the small piece of code or pseudocode that makes the diagram real. The goal is not to memorize Claude Code’s feature list. It is to see the handful of ideas that every serious coding agent now shares, so you can reason about any of them in an interview or a design review.
By the end you’ll be able to:
- Draw the agentic loop and name where a tool call, a hook, and a verification check each enter it.
- Explain why
CLAUDE.mdand an output style change behavior in mechanically different ways. - Estimate the token cost of a long session from prompt-cache multipliers.
- Tell apart the three “remote” mechanisms that people constantly conflate.
- Describe how an agent verifies a UI change with a screenshot, and why that same power is a security risk.
A note on sourcing: Claude Code moves fast, so specific version numbers and model names date quickly. Every load-bearing claim below links to an Anthropic primary source. Where something is not publicly disclosed, we say so rather than guess.
Claude Code is the harness; Claude is the model inside it
Start with the single most useful sentence in the docs. Anthropic’s own glossary defines the agentic harness as “the tools, context management, and execution environment that turn a language model into a capable coding agent,” and states it plainly: “Claude Code is the harness; Claude is the model inside it. The harness supplies file access, shell execution, permission gating, memory loading, and the loop that chains actions together.”
Hold onto that split. The model does one thing: it reads text and writes text. It cannot open a file, run a test, or click a button. Every capability you think of as “the agent doing something” is the harness executing an action on the model’s behalf and feeding the result back as more text. Once you internalize this, the rest of Claude Code stops being a grab-bag of features and becomes layers around one loop.
The loop that makes it an agent
Anthropic describes the agentic loop as four beats: “gather context, take action, verify results, and repeat until done” (glossary). That is the whole engine. Everything else in this lesson plugs into one of those beats.
flowchart LR A["Gather context<br/>files, memory, tool results"] --> B["Model decides<br/>text or a tool call"] B -->|"tool call"| C["Harness runs the tool"] C --> D["Append result<br/>as tool_result"] D --> A B -->|"no tool, done"| E["Turn ends"]
Under the hood the loop is not clever. It is a while loop around one API call. The model returns either a final answer or a request to use a tool, the harness runs the tool, appends the result, and calls again. Here is the shape, matching the tool-use docs:
messages = [{"role": "user", "content": task}]
while True:
reply = model.create(messages=messages, tools=TOOLS)
messages.append({"role": "assistant", "content": reply.content})
if reply.stop_reason != "tool_use":
break # model answered; loop is done
results = []
for block in reply.content:
if block.type == "tool_use":
output = run_tool(block.name, block.input) # the harness acts
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output,
})
messages.append({"role": "user", "content": results})
The signal that drives the loop is stop_reason. When the model wants to act, the reply carries stop_reason: "tool_use" and a tool_use block naming the tool and its arguments. The harness matches its answer back with the tool_use_id. No tool_use, and the loop exits with the model’s final text. That while loop is the difference between a chatbot and an agent.
A tool call is a contract, not a function
The model never runs code. It emits a request, and the harness decides whether and how to honor it. That request is three pieces of JSON: the tool definition you send in (name, description, input schema), the tool_use block the model sends back (the tool it picked and the arguments), and the tool_result you return. The tools lesson covers designing that surface in depth. The one fact to carry into the rest of this lesson: because the harness sits between the model and every real action, the harness is exactly where memory, permissions, hooks, and verification get to intervene.
The line to remember for this session: an agent is a text model wrapped in a loop that runs its tool requests and feeds back the results.
What is actually in the context window
The model sees only what the harness puts in front of it. The glossary enumerates the context window as “conversation history, file contents, command outputs, CLAUDE.md, auto memory, loaded skills, and system instructions.” Picture the harness as an onion around the model, each layer contributing text to the window:
flowchart TB
subgraph WINDOW["What the model reads each call"]
SYS["System instructions<br/>(+ output style appended)"]
MEM["CLAUDE.md + auto memory<br/>(injected as a user message)"]
SKILLS["Loaded skills"]
HIST["Conversation history<br/>+ tool results"]
FILES["File contents, command output"]
end
MODEL["Claude (the model)"]
WINDOW --> MODEL
Two of these layers change behavior in ways that look identical but are mechanically different, and interviewers love the distinction.
An output style modifies the system prompt itself. The output-styles docs say a style changes “how Claude responds, not what Claude knows,” and its instructions are appended to the end of the system prompt.
CLAUDE.md does not touch the system prompt. The glossary states it “is injected as a user message after the system prompt.” All discovered CLAUDE.md files (project, user, managed) are concatenated broadest-to-most-specific, and the project-root file survives compaction and is re-read from disk.
Why care? Because a system-prompt change is read once at session start and needs a new session to take effect, while a user-message injection like CLAUDE.md is re-applied and can be refreshed mid-session. Same visible effect (“the agent now follows my rule”), different plumbing, different failure modes.
Memory that does not fit gets compacted, not forgotten cleanly
The context window is finite. When it fills, the harness runs compaction: the glossary says it “clears older tool outputs first, then summarizes.” Verbose tool results (a 2,000-line test log) are the first to go, because they are the cheapest to drop and the least likely to be needed verbatim later.
flowchart LR
F["Window near full"] --> G["Drop oldest<br/>tool outputs"]
G --> H{"Still over<br/>budget?"}
H -->|yes| I["Summarize older<br/>history into a digest"]
H -->|no| J["Continue"]
I --> J
The mechanism you would write to decide when to compact is a token accountant:
def maybe_compact(messages, limit, headroom=0.15):
used = count_tokens(messages)
if used < limit * (1 - headroom):
return messages
kept = drop_oldest_tool_outputs(messages) # step 1: cheap
if count_tokens(kept) >= limit * (1 - headroom):
kept = summarize_old_turns(kept) # step 2: lossy
return kept
You can trigger this yourself with /compact and inspect the window with /context (glossary). The teaching point: an agent’s “memory” is not a database, it is a budget the harness manages, and summarization is lossy, so anything you truly need again belongs in CLAUDE.md or a file, not in the scrollback.
Why a one-line question in an all-day session still costs money
Long agent sessions resend a large, mostly-unchanged prefix on every single loop iteration. Without help, that would be ruinously expensive. Prompt caching is what makes it viable, and the economics are worth knowing exactly.
From Anthropic’s pricing docs, relative to the base input rate: a 5-minute cache write costs 1.25x, a 1-hour cache write costs 2x, and a cache read costs 0.1x. A cache hit is a tenth of the price of fresh input.
flowchart LR P["Stable prefix<br/>(system, CLAUDE.md, files)"] -->|"first call: write 1.25x"| CACHE["Prompt cache"] CACHE -->|"every later call: read 0.1x"| CALL["This turn"] NEW["Your new one-line message"] -->|"input 1.0x"| CALL CALL -->|"output 1.0x"| OUT["Reply + thinking tokens"]
The arithmetic that follows:
# Opus-class rates as an example: input $5 / MTok, output $25 / MTok
prefix = 40_000 tokens, cached
new_msg = 200 tokens, fresh
output = 600 tokens (includes thinking)
first turn : 40000 * (5 * 1.25)/1e6 = $0.250 # cache write
later turn : 40000 * (5 * 0.10)/1e6 = $0.020 # cache read
+ 200 * (5 )/1e6 = $0.001 # fresh input
+ 600 * (25 )/1e6 = $0.015 # output
later turn total ≈ $0.036
That is why an all-day session stays affordable: after the first turn, the 40k-token prefix costs two cents to re-read instead of twenty to re-send. In Claude Code the cache lifetime is one hour on a subscription and drops to five minutes on API keys or once you draw on usage credits, per the costs docs. The break-even is one cache read for the 5-minute tier, two for the 1-hour tier. The line to remember: an agent’s cost is dominated by how much stable context it can keep warm in cache, not by the length of your last message.
Thinking is output you pay for, tuned by a dial
Claude can spend tokens reasoning before it answers. Those thinking tokens are billed as output tokens, and Claude Code exposes an effort level from low to max (default high) that trades reasoning spend against capability (model-config, costs).
The counterintuitive part, from Anthropic’s computer-use guidance: on some models a low effort setting uses fewer total output tokens than turning thinking off entirely, “because fewer mistakes mean fewer retries.” More thinking up front can be cheaper than a cheap-looking answer that fails a check and forces the loop to run again. This is the first hint of the theme that dominates the second half of this lesson: an agent’s cost and quality are set by how often it has to redo work, and verification is what controls that.
Four ways to change what the agent does, and how each one works
Claude Code gives you four customization surfaces. They feel similar (each “tells the agent to do something”) but they intervene at different points in the loop, which is exactly what an interviewer will probe.
Hooks: deterministic code at fixed points in the loop
A hook is your code that the harness runs at a fixed lifecycle event, not at the model’s discretion. The hooks docs list events including UserPromptSubmit, PreToolUse, PostToolUse, Stop, and session start/end. Overlay them on the loop:
flowchart LR S["SessionStart"] --> U["UserPromptSubmit"] U --> M["Model decides"] M -->|tool call| PRE["PreToolUse<br/>(can block)"] PRE -->|allow| T["Run tool"] T --> POST["PostToolUse"] POST --> M M -->|done| ST["Stop<br/>(can prevent stopping)"]
The mechanism is a subprocess contract. On a PreToolUse event the harness sends the tool call as JSON on stdin, and your hook decides its fate through the exit code or a JSON verdict on stdout. Exit 2 blocks; a JSON permissionDecision of allow, deny, or prompt steers it:
// .claude/settings.json
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "./guard.sh" }]
}]
}
}
#!/usr/bin/env bash
# guard.sh: receives the tool call as JSON on stdin
payload=$(cat)
if echo "$payload" | grep -q 'rm -rf'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"blocked destructive command"}}'
exit 0
fi
exit 0 # allow
Because hooks fire deterministically and can block, they are how you enforce a rule the model cannot talk its way around. The most common use, per the costs docs, is a PreToolUse hook that trims a huge test log down to its FAIL lines before Claude ever sees it, saving both tokens and attention.
Skills: instructions loaded only when needed
A skill is a folder with a SKILL.md entrypoint (YAML frontmatter plus a Markdown body). Its power is progressive disclosure, which the Agent Skills post frames as “a well-organized manual: table of contents, then chapters, then appendix.” Three loading levels:
flowchart TB L1["Level 1: name + description<br/>always in context, ~dozens of tokens"] L2["Level 2: full SKILL.md body<br/>loaded only when invoked"] L3["Level 3: bundled files + scripts<br/>loaded on demand; scripts can run<br/>without loading their text"] L1 --> L2 --> L3
Under the hood the harness keeps only the tiny metadata in the window at all times, so the model knows the skill exists without paying for its contents. When the skill is triggered, the body loads; bundled scripts can execute without their source ever entering context. A minimal skill:
---
description: Summarize the current git diff for a commit message.
allowed-tools: Bash(git diff:*)
---
Run !`git diff --staged` and write a one-line summary in imperative mood,
then a short body explaining why.
The !`command` runs a shell command and inlines its output into the prompt before Claude reads it. That is how a skill can be dynamic without you writing any harness code. Progressive disclosure is the design idea to steal: expose a cheap index of capabilities, and pay for the details only on use.
(Worth knowing for currency: the docs note that custom slash commands have been merged into skills, so a .claude/commands/deploy.md and a .claude/skills/deploy/SKILL.md both create /deploy. Treating them as two separate systems is now out of date, per the skills docs.)
Plan mode: read, propose, wait
Plan mode is a permission mode where Claude can read, search, and explore but cannot edit source until you approve a written plan (permission-modes). It is a small state machine:
stateDiagram-v2 [*] --> Exploring Exploring --> Exploring: read / search (edits blocked) Exploring --> PlanReady: present plan PlanReady --> Editing: approve PlanReady --> Exploring: keep planning
Cycle into it with Shift+Tab. The value is that expensive, hard-to-reverse edits wait behind a cheap, reviewable artifact.
Permissions: deny beats allow, always
Every tool call is gated by permission rules with three arrays, allow, deny, and ask. The evaluation order is fixed: deny, then ask, then allow, first match wins (glossary).
flowchart LR
C["Tool call"] --> D{"deny match?"}
D -->|yes| X["Blocked"]
D -->|no| A{"ask match?"}
A -->|yes| Q["Prompt the user"]
A -->|no| AL{"allow match?"}
AL -->|yes| R["Run"]
AL -->|no| Q
// .claude/settings.json: deny wins even against a broad allow
{
"permissions": {
"deny": ["Read(./.env)", "Bash(rm:*)"],
"allow": ["Bash(npm run test:*)", "Read(./src/**)"],
"ask": ["Bash(git push:*)"]
}
}
One subtlety that trips people up, from the settings docs: across settings layers, scalar values override by precedence, but permission arrays merge. A deny rule in your user settings still applies inside a project that only listed allows. The line to remember: hooks, skills, plan mode, and permissions all intervene in the same loop, but at different beats, code before a tool runs, instructions when a task starts, a gate before edits, and a rule on every call.
Subagents give verbose work its own context window
When a task involves a lot of noisy output (reading a large codebase, processing logs, fetching docs), running it in the main conversation would flood the window. Claude Code runs it in a subagent instead: a separate context window whose only return value to the main thread is a summary (costs docs). The multi-agent lesson goes deeper; the mechanism in one picture:
flowchart TB MAIN["Main agent<br/>(clean context)"] -->|"spawn: 'find every call site'"| SUB["Subagent<br/>(own context window)"] SUB -->|"reads 40 files, runs greps"| SUB SUB -->|"returns a short summary"| MAIN
def delegate(task):
sub_ctx = new_context_window() # isolated from the main thread
result = run_agent_loop(task, sub_ctx) # burns tokens here, not there
return summarize(result) # only this returns to main
The main thread pays for the summary, not the 40 files the subagent read. This is the same idea as compaction (protect the scarce main window) applied preemptively.
The three “remote” mechanisms, kept straight
This is the single most-conflated area in the whole product, so pin the distinctions down. There are three different things that all sound like “running Claude somewhere else.”
flowchart TB
subgraph CLOUD["1. Cloud session"]
direction LR
L1["Your laptop / phone<br/>(just a client)"] --> V1["Anthropic VM<br/>Claude runs HERE"]
end
subgraph RC["2. Remote Control"]
direction LR
L2["Web / mobile UI<br/>(a window)"] --> V2["Your machine<br/>Claude runs HERE"]
end
subgraph CI["3. GitHub Action"]
direction LR
EV["Issue / cron event"] --> V3["CI runner<br/>Claude runs HERE"]
end
- Cloud sessions: Claude runs in an Anthropic-managed VM. You kick one off with
claude --cloud "task"and monitor it from any device; the compute is remote (claude-code-on-the-web). - Remote Control: Claude keeps running on your own machine; the web or mobile UI is “just a window into that local session,” so your files, MCP servers, and tools stay local. It makes outbound HTTPS only and opens no inbound ports (remote-control).
- GitHub Actions:
claude-code-actionruns Claude Code inside a CI runner, either interactively on an@claudemention or on any event includingschedule(cron) (github-actions).
# .github/workflows/claude.yml: responds to @claude on issues
on:
issue_comment:
types: [created]
jobs:
claude:
if: contains(github.event.comment.body, '@claude')
runs-on: ubuntu-latest
permissions: { contents: write, pull-requests: write, id-token: write }
steps:
- uses: actions/checkout@v6
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
A cloud session and a terminal session can hand off to each other. claude --teleport <id> pulls a running cloud session into your terminal: it checks you are in the right repo, fetches and checks out the cloud branch, and loads the full history (claude-code-on-the-web). The line to remember: “remote” is three different architectures, and the question that separates them is simply “on which machine is the model’s loop running?”
Give it a check it can run, or you become the check
This is the heart of what makes a coding agent useful instead of merely fluent. Anthropic’s best-practices doc puts it bluntly: “Claude stops when the work looks done. Without a check it can run, ‘looks done’ is the only signal available, and you become the verification loop: every mistake waits for you to notice it. Give Claude something that produces a pass or fail, and the loop closes on its own.”
The “check” is anything that returns a signal the model can read: “a test suite, a build exit code, a linter, a script that diffs output against a fixture, or a browser screenshot compared against a design.” Fold that back into the loop from the first session and the fourth beat lights up:
flowchart LR
A["Take action<br/>(edit code)"] --> B["Run the check<br/>(test / build / lint / screenshot)"]
B --> C{"Pass?"}
C -->|no| D["Read the failure"]
D --> A
C -->|yes| E["Stop"]
Under the hood the check is just another tool call whose result re-enters the loop as ground truth:
while True:
apply_edit(plan)
result = run_tool("Bash", {"command": "npm test"}) # ground-truth signal
if "FAIL" not in result and exit_code == 0:
break
plan = revise_from(result) # the failure text drives the next edit
Anthropic frames the underlying pattern as evaluator-optimizer: “one LLM call generates a response while another provides evaluation and feedback in a loop,” a fit “when we have clear evaluation criteria” (building-effective-agents). Coding is the ideal case because tests give an unambiguous pass or fail.
There are four escalating ways to enforce the stop, from soft to deterministic (best-practices):
flowchart LR P1["1. In-prompt<br/>'run the tests and iterate'"] --> P2["2. /goal<br/>evaluator re-checks each turn"] P2 --> P3["3. Stop hook<br/>blocks the turn until the check passes"] P3 --> P4["4. Review subagent<br/>a fresh model tries to refute the result"]
The strongest of these is the last: have a fresh context try to refute the work, “so the agent doing the work isn’t the one grading it.” An author grading its own homework is the failure mode; an independent verifier is the fix.
Looping through screenshots: two different mechanisms
The most striking thing a coding agent does is fix a UI by looking at it. There are two distinct systems here, and conflating them is a classic mistake.
The first is the API computer use tool, which drives a full desktop. Its loop is literally screenshot, act, screenshot, repeated. Critically, per the computer-use docs, “Claude doesn’t directly connect to this environment”: your application takes the screenshot, runs the click, and returns the new screenshot as a tool_result.
sequenceDiagram
participant M as Claude
participant H as Your harness
participant D as Screen (Xvfb / browser)
M->>H: tool_use {action: screenshot}
H->>D: capture
D-->>H: image
H-->>M: tool_result (image)
M->>H: tool_use {action: left_click, [500,300]}
H->>D: click
H-->>M: tool_result (new screenshot)
Note over M,H: repeat until the screen matches the goal
Because “Claude sometimes assumes outcomes of its actions without explicitly checking their results,” the docs recommend this exact system-prompt instruction, which is the plain-English embodiment of the whole loop:
After each step, take a screenshot and carefully evaluate if you have
achieved the right outcome. Explicitly show your thinking: "I have
evaluated step X..." If not correct, try again. Only when you confirm a
step was executed correctly should you move on to the next one.
The second mechanism is what Claude Code itself uses for front-end work: not the desktop tool, but a browser workflow. You paste a design, and Claude builds it, opens it, screenshots the result, and compares. The recommended prompt, from best-practices:
[paste screenshot] implement this design. take a screenshot of the
result and compare it to the original. list differences and fix them.
Claude Code drives this through its Chrome integration (chrome docs), which can read console errors and the DOM and capture screenshots, so the loop closes visually. The distinction to keep: the desktop computer-use API outputs pixel coordinates and runs in a sandboxed virtual display, while Claude Code’s UI verification is a browser workflow. Both close the same loop (build, look, compare, fix), but they are different tools. The line to remember: a screenshot is just another tool_result, which is why “verify your work” and “look at the screen” are the same mechanism wearing two faces.
Why the harness holds the keys, not the model
Giving an agent a shell and a browser is dangerous, so the harness contains it. On Linux, Claude Code uses bubblewrap; on macOS, Seatbelt (sandbox-exec). Filesystem access is confined to the working directory, and network access is forced through a proxy that validates domains, applied to spawned subprocesses too. Anthropic reports this “safely reduces permission prompts by 84%” (sandboxing post).
flowchart TB MODEL["Claude proposes an action"] --> SB["OS sandbox<br/>bubblewrap / Seatbelt"] SB --> FS["Filesystem: cwd only"] SB --> NET["Network: via domain-validating proxy"] CREDS["Git credentials, signing keys"] -.->|"held OUTSIDE the sandbox"| PROXY["Secure proxy"] NET --> PROXY
The credential design is the sharp part: in cloud sessions, git credentials and signing keys are never inside the sandbox. Auth goes through a proxy using scoped, short-lived credentials (claude-code-on-the-web). So even if the agent is fully compromised by malicious input, it cannot read the secrets that would let it do lasting damage. This matters because agents read untrusted text (web pages, issues, logs) all day, and prompt injection is real: a malicious web page can try to make the agent read a secret and send it somewhere. Other agent platforms have shipped browser agents without this credential discipline and leaked cloud keys as a result. The containment layer is not paranoia, it is the price of letting an agent run a shell at all.
Putting it together
Every subsystem in this lesson is a layer around one while loop. Context, memory, and caching decide what the model reads and what it costs. Hooks, skills, plan mode, and permissions bend the loop at specific beats. Subagents and the three remotes decide where and how widely it runs. Verification and screenshots close the loop so “looks done” becomes “is done.” And the sandbox makes it safe to hand the loop a shell.
If you remember one thing: a coding agent is a text model in a loop, and every hard problem, cost, memory, safety, correctness, is solved by controlling what enters the loop and what is allowed to leave it.