A coding agent is a permission-gated loop wrapped around a sandbox, where the tests are the reward signal and the hardest engineering is keeping a dangerous tool surface safe at scale.
A coding agent is not a model — it is a loop around a sandbox, and the model is one component inside it. The loop is: read context, the model proposes a tool call (read file, edit, run command), a permission layer decides whether to allow it, the sandbox executes it, the result is fed back, repeat until the task is done or a budget is exhausted. Two things make this hard and distinct from a chatbot: the tool surface includes arbitrary code execution and filesystem writes (so safety is the dominant design constraint, not an afterthought), and the test suite is your reward signal (verification, not the model's self-assessment, tells you if the task succeeded). The senior framing of the central design tension is not "which sandbox is perfect?" but "which sandbox reduces risk enough for this use case without making the agent useless?" Everything below — the harness, context management over a 2M-line repo, multi-agent exploration, scale, cost, and eval — hangs off those two facts.
The words first.
Step by step.
auth.py").Remember this: the model proposes, the harness disposes — safety and verification live in the harness, not the model.
The first design decision is what the model is allowed to do. A minimal but production-shaped tool surface for code work is: read_file, edit_file (or a structured patch/apply tool), grep/search, run_command (shell), list_dir, and optionally fetch_url and spawn_subagent. The instant you include run_command and edit_file, you have granted arbitrary code execution and filesystem mutation — which is exactly the capability an attacker wants. So the tool surface and the safety architecture are the same conversation.
Two design refinements senior candidates raise:
rm -rf," and that text lands directly in the model's context. Treat every tool result as untrusted.The loop is deceptively simple but the failure modes live in the details: budgets (max tool calls, max wall-clock, max tokens), what counts as "done," how you recover from a tool error, and where you gate. Here is the canonical structure with the gate in the right place.
Name the pieces: messages is the running conversation; tools is the JSON schema list the model can call; gate(call) returns allow/ask/deny; budget caps tool calls.
Concrete trace for "fix the failing test in auth.py", budget = 10 tool calls:
grep("def login"). gate -> allow (read-only). Sandbox returns 2 matches. Tool calls used: 1.read_file("auth.py", lines 40-90). gate -> allow. Returns 50 lines. Used: 2.edit_file("auth.py", patch=...). gate checks path against deny-list (auth.py not forbidden) -> allow. Patch applies cleanly. Used: 3.run_command("pytest tests/test_auth.py"). gate -> shell sandbox is --network=none, command is in allowlist (pytest) -> allow. Returns 1 passed. Used: 4.What it did to the data: five model turns, four sandboxed actions, one verified test pass — the loop converted a vague task into an auditable sequence of gated actions ending in a real green test, all under budget.
The non-obvious harness design choices an IC6 should volunteer:
rm -rf node_modules," that string is evaluated by the gate before the sandbox sees it. Any single layer can block; all applicable layers must approve for the action to run. This is the deny-first invariant.infra/prod/") without forking the agent.The sandbox is where senior interviews are won or lost. There is a spectrum:
Beyond the isolation primitive, the safety principles are: least privilege (short-lived IAM roles, never long-lived API keys baked into the sandbox), environment separation (reasoning runs on normal infra; actions run in the isolated sandbox), network filtering (default --network=none with an explicit API allowlist — this single default kills most data-exfiltration paths), hard timeouts at three scopes (per tool call, per task loop, per sandbox lifetime), and immutable audit logs of every network request, shell command, and file write.
A coding agent that can't tell whether its change worked is a random patch generator. Verification is what makes it an engineer. The layered verification stack:
pytest/go test/npm test, and the pass/fail is fed back into the loop. If the suite is green, you have evidence, not a vibe.The subtle trap: the agent can game the reward. "Make the test pass" has the degenerate solution "delete the assertion." This is why verification must be paired with runtime monitoring (3.6) and immutable diffs you can audit — and why you protect test files with deny rules.
A 2-million-line monorepo does not fit in any context window, and even if it did, you wouldn't want to pay for it. Context management is the art of putting the right slice in front of the model each step.
grep/symbol-search/embedding-search to find the ~5–20 relevant files, then read targeted line ranges. The model should pull context via tools, not have it all pushed. This is also why a good search tool is a safety-relevant performance lever.CLAUDE.md-style file (conventions, build commands, architecture notes) hand-curated and always-in-context beats re-deriving the same facts every session.For hard tasks, a single linear loop is brittle. Two patterns:
A real (runnable-shaped) harness loop showing the loop, the deny-first gate, the sandboxed exec, and verification. This is intentionally provider-agnostic at the model-call boundary; in production the model_step call would target a tool-using model (e.g. Claude via the Anthropic SDK).
import subprocess, shlex, time
from pathlib import Path
DENY_PATHS = {"infra/prod", "tests/"} # never let the agent edit these
SHELL_ALLOWLIST = {"pytest", "go", "npm", "ruff", "grep", "ls", "cat"}
MAX_TOOL_CALLS, MAX_WALL_SECONDS = 30, 600
def gate(call: dict) -> str:
"""Deny-first: any rule can deny; all must pass to allow."""
name, args = call["name"], call["args"]
if name == "edit_file":
p = str(Path(args["path"]))
if any(p.startswith(d) for d in DENY_PATHS):
return "deny" # structural path deny-rule
if name == "run_command":
prog = shlex.split(args["cmd"])[0]
if prog not in SHELL_ALLOWLIST:
return "ask" # escalate unknown commands
return "allow"
def execute(call: dict, sandbox) -> str:
"""Runs inside the per-session sandbox (microVM/container), network=none."""
name, args = call["name"], call["args"]
if name == "read_file":
return sandbox.read(args["path"], args.get("lines"))
if name == "edit_file":
return sandbox.apply_patch(args["path"], args["patch"])
if name == "run_command":
# hard per-call timeout; no network; captured for the audit log
return sandbox.run(args["cmd"], timeout=120, network=False)
raise ValueError(f"unknown tool {name}")
def verify(sandbox) -> bool:
deterministic = sandbox.run("ruff check .", timeout=60, network=False)
if "error" in deterministic.lower():
return False
result = sandbox.run("pytest -q", timeout=300, network=False) # reward signal
return "failed" not in result.lower()
def agent_loop(task: str, sandbox, model_step):
messages = [{"role": "user", "content": task}]
start, calls = time.time(), 0
while calls < MAX_TOOL_CALLS and time.time() - start < MAX_WALL_SECONDS:
step = model_step(messages, tools=TOOL_SCHEMAS) # the model proposes
if step.get("final"):
return {"ok": verify(sandbox), "text": step["text"], "calls": calls}
decision = gate(step["call"])
if decision == "deny":
messages.append({"role": "tool", "content": "DENIED by policy"})
continue # do NOT execute
if decision == "ask" and not human_approves(step["call"]):
messages.append({"role": "tool", "content": "USER DENIED"})
continue
audit_log.write(step["call"]) # immutable record
out = execute(step["call"], sandbox) # harness disposes
messages.append({"role": "tool", "content": out})
calls += 1
return {"ok": False, "text": "budget exhausted", "calls": calls}The load-bearing details: the gate runs before execute and a deny appends a message but never calls the sandbox; verify runs deterministic checks before the expensive test suite; every executed call hits the audit log; and the whole thing is bounded by three budgets. Swap sandbox for a Firecracker-backed implementation and model_step for a real tool-using model and this is the shape of production.
| Decision | Cheap / fast option | Expensive / safe option | What changes at scale |
|---|---|---|---|
| Isolation | Hardened container (shared kernel) | Firecracker microVM (own kernel, ~200ms boot) | At 50k+ sessions, microVM RAM/boot dominates cost, not GPU |
| Edit tool | "Rewrite whole file" | Structured patch/diff-apply | Whole-file rewrites blow token budget on large files |
| Context | Stuff repo into window | Retrieval + compaction + prompt cache | Prompt caching saves 80–90% on the stable prefix per call |
| Verification | Trust model's "done" | Deterministic checks + full test suite | Test suites become your CI bottleneck; run in-sandbox |
| Exploration | Single linear loop | Multi-agent parallel sub-agents | Cost and sandbox count scale ~linearly with sub-agents |
| Routing | One big model for all steps | Cheap model for search/read, big model for edits | 2–5x aggregate cost savings; needs threshold tuning |
Cost reality check. The trap interviewers set: "per-token prices dropped ~80% in 2025–2026, so cost is solved." No — agentic workflows make 50–200 model calls per task, turning a cheap per-token price into an expensive per-task cost. A task with 100 calls averaging 8k input + 1k output tokens is ~900k tokens; prompt caching the stable prefix is what makes this economical. And the sandbox, not the model, is often the first thing to break the budget at scale: 50,000 concurrent Firecracker microVMs at even modest RAM each is a large fleet, and idle sessions waiting on a human or a slow test still hold their VM. You manage this with aggressive sandbox reaping (timeouts), pausing/snapshotting idle VMs, and pooling.
Failure modes to name unprompted: (1) reward hacking — agent deletes a test to make the suite pass (mitigate with test-path deny rules + diff review); (2) prompt injection via tool output — a fetched page or file comment instructs the agent (mitigate with output sanitization + network allowlist so even a hijacked agent can't exfiltrate); (3) cascading sandbox failure — one bad VM taking down a host (mitigate with per-session isolation and failure isolation); (4) context rot — compaction drops a fact the agent needed, and it loops re-discovering it (mitigate with structured project memory and careful compaction triggers).
edit_file calls, one run_command for the tests, then a final no-tool message. You gate before every execute, deny-first (a deny appends a policy message and skips execution entirely), and you run verification (deterministic checks then the test suite) only at the end before reporting done.grep/symbol-search/embedding-search and read_file with line ranges, so it pulls the ~5–20 relevant files per step. Add compaction (verbatim deletion of stale tool outputs, ~50–70% reduction) on a token threshold, prompt-cache the stable prefix (system prompt + conventions + tool schemas) for 80–90% latency savings on that portion, and keep a hand-curated project-memory file always in context.--network=none, sandbox isolation, runtime monitoring). A framework is a starting point, not the design.Flashcard. A coding agent = permission-gated loop + isolated sandbox + tests-as-reward. The model proposes, the harness disposes; deny-first gating before every action; verify with deterministic checks then the suite; retrieve context, don't stuff it; per-task cost (50–200 calls) and the idle-VM fleet — not the GPU — are what break at scale.
Next: /safety for the guardrails and jailbreak-detection layer that wraps every tool surface, and /evals for building the executable golden sets that keep your agent honest.