🔧 Harness Engineering — Production Scaffolding for AI Systems¶
Target: Staff/Principal Engineer | Focus: Designing the infrastructure layer that makes AI models safe, observable, and controllable in production
30-second answer
The harness is everything around the model weights: the system prompt and instruction files, the tool set, the sandbox the tools run in, permission checks, verification (tests, validators, judges), memory and context management, budgets, and tracing. You design it as a control system: guides steer the model before it acts, sensors check what it did afterwards. Prefer cheap deterministic checks (schemas, tests, linters) over expensive model-based ones, and treat anything the model reads from the outside world as untrusted input that can hijack it. The model is not your security boundary; the harness is.
1. WHAT IS A HARNESS?¶
In AI engineering, the harness is everything surrounding the model that enables it to operate reliably in production. The shorthand:
Agent = Model + Harness
A model without a harness can chat, but cannot reliably act, access external data, or follow organizational constraints. The harness provides:
| Layer | Function | What It Enables |
|---|---|---|
| Execution Infrastructure | Sandbox for running code, calling APIs, querying databases | Safe tool execution |
| Guardrails | Safety rules, permissions, human-in-the-loop | Controlled behavior |
| Verification | Tests, validators, LLM-as-a-judge | Self-correction |
| Memory & Context | RAG pipelines, state management, context compaction | Long-running tasks |
| Observability | Tracing, logging, evaluation metrics | Auditability, debugging, cost attribution |
┌──────────────────────────────────────────────────────────────┐
│ AGENT HARNESS │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │
│ │ FEEDFORWARD │ │ EXECUTION │ │ FEEDBACK │ │ STATE │ │
│ │ (Guides) │ │ (Sandbox) │ │ (Sensors) │ │ (Memory)│ │
│ │─────────────│ │─────────────│ │─────────────│ │─────────│ │
│ │ System │ │ Code exec │ │ Unit tests │ │ Context │ │
│ │ prompt │ │ API calls │ │ Linters │ │ window │ │
│ │ CLAUDE.md / │ │ DB queries │ │ Schema │ │ Progress│ │
│ │ AGENTS.md │ │ File I/O │ │ checks │ │ files │ │
│ │ Skills │ │ │ │ LLM judge │ │ Long- │ │
│ │ Plans │ │ │ │ Diff review │ │ term │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────┘ │
└──────────────────────────────────────────────────────────────┘
The guides/sensors vocabulary comes from Birgitta Böckeler's 2026 harness-engineering article on martinfowler.com. She adds a second axis that is useful in interviews: each guide or sensor is either computational (deterministic, fast, cheap: a linter, a type checker, a test) or inferential (model-based, semantic, slower and costlier: an LLM reviewer). Use computational controls wherever they can express the rule, and spend inferential ones on what only judgement can catch.
1.1 Why Harness Engineering Now?¶
Two things changed. Models became good enough to run multi-step tasks with tools for long stretches, and several vendors now ship models of comparable capability. So the failures teams see in production are mostly harness failures: the agent had the wrong context, an unsafe tool, no way to check its work, or no budget limit.
| Era (rough framing) | Focus | Typical question |
|---|---|---|
| 2022-2023 | Model capability | "Which model is smarter?" |
| 2024-2025 | Prompt and context engineering | "What do we put in the context window?" |
| 2025-2026 | Harness engineering | "What tools, checks and limits surround the model?" |
These eras overlap; treat the table as a mental model, not history.
Figure: Agent = Model + Harness; the harness wraps the model with guides, execution, sensors and state.
flowchart LR
G["Feedforward guides: prompt, CLAUDE.md, skills, plan"] --> M["Model"]
M --> E["Execution sandbox: code, APIs, DB, files"]
E --> S["Feedback sensors: tests, linters, schema, LLM judge"]
S --> M
ST["State: context, progress files, memory"] <--> M
2. TYPES OF HARNESSES¶
2.1 Evaluation Harnesses (Testing/Benchmarking)¶
Focused on measurement. An evaluation harness runs a model or agent against a fixed set of tasks and computes metrics, so you can compare versions and catch regressions.
┌──────────────────────────────────────────────────┐
│ EVALUATION HARNESS │
│ │
│ Task set ──→ Agent/Model ──→ Grader ──→ Report │
│ │
│ Dimensions: │
│ · Correctness / task success rate │
│ · Latency (p50, p95, p99) │
│ · Cost per task (not per request) │
│ · Safety / policy violations │
│ · Adversarial robustness (injection, jailbreak) │
└──────────────────────────────────────────────────┘
Points interviewers probe:
- Non-determinism: run each task several times and report a pass rate with variance, not a single pass/fail. For agents, distinguish "succeeds at least once in k tries" (pass@k) from "succeeds every time in k tries" (pass^k); the second is what users experience.
- Grade the outcome, and sometimes the trajectory: check the final state (tests pass, ticket closed) first; inspect the path (tool calls, wasted steps, unsafe actions) when diagnosing.
- Environment noise: flaky sandboxes, rate limits and timeouts can move agentic scores by several points. Fix the infrastructure before you trust small deltas.
- Contamination: public benchmarks may be in training data. Keep a private, task-representative set built from real traffic.
Common tools (check current status before naming one in an interview):
| Tool | Focus | Notes |
|---|---|---|
| Inspect (UK AI Security Institute) | Agent and model evals | Open source, sandboxed agent tasks |
| promptfoo | Prompt and app regression tests | CLI/CI-friendly, red-teaming |
| DeepEval | LLM eval framework | Pytest-style |
| lm-evaluation-harness (EleutherAI) | Standard academic benchmarks | Model-level, not app-level |
| LangSmith, Braintrust, Arize Phoenix | Tracing plus evals on production data | Online and offline evaluation |
2.2 Agent Harnesses (Operational Scaffolding)¶
Focused on reliability and control. The production infrastructure that guides and constrains agent behavior.
┌─────────────────────────────────────────────────────┐
│ AGENT HARNESS │
│ │
│ 1. Feedforward Controls (guides) │
│ ├── System prompt (role, constraints) │
│ ├── Repo instructions (CLAUDE.md, AGENTS.md) │
│ ├── Skills (instructions loaded on demand) │
│ └── Plan (pre-committed execution path) │
│ │
│ 2. Execution Environment │
│ ├── Sandboxed container / microVM │
│ ├── Scoped, short-lived credentials │
│ └── Resource limits (CPU, memory, time, tokens) │
│ │
│ 3. Feedback Sensors │
│ ├── Schema validation of tool inputs/outputs │
│ ├── Tests, type checks, linters │
│ └── LLM-as-a-judge / reviewer agent │
│ │
│ 4. Safety Interlocks │
│ ├── Permission rules + human approval gates │
│ ├── Rate limiters / circuit breakers │
│ └── Budgets and kill switches │
└─────────────────────────────────────────────────────┘
Claude Code is a useful public example of an agent harness: its documentation describes permission rules (allow / ask / deny per tool), hooks that run your own scripts before and after tool calls, CLAUDE.md memory files, skills, subagents and an optional sandbox for the Bash tool. You can cite those documented features; don't claim knowledge of how they are implemented internally.
3. HARNESS COMPONENTS — DEEP DIVE¶
3.1 Guardrails¶
Guardrails are the safety boundaries around agent behavior. They operate at multiple levels:
| Level | Guardrail | Implementation |
|---|---|---|
| Input | Injection / jailbreak screening | Classifier models (e.g. Llama Guard, Prompt Guard), heuristics. Reduces risk; never sufficient alone (see 3.5) |
| Input | Topic/scope filtering | Small classifier or embedding similarity to allowed topics |
| Output | PII/secret redaction | Pattern matching + NER-based detection (e.g. Microsoft Presidio), secret scanners |
| Output | Harmful-content filtering | Moderation classifier (provider moderation endpoint or self-hosted). Google's Perspective API is being shut down at the end of 2026, so don't design around it |
| Tool | Parameter validation | JSON Schema validation (strict tool schemas where the API supports them) |
| Tool | Authorization | Check the end user's permissions per tool and per resource, not just the agent's |
| Execution | Rate limiting | Token bucket, sliding window |
| Execution | Budget control | Token / dollar / step budget per session |
A minimal pipeline (sketch; Guardrail and GuardrailResult are your own types):
from dataclasses import dataclass, field
from typing import Protocol
@dataclass
class GuardrailResult:
passed: bool
reason: str = ""
class Guardrail(Protocol):
def check(self, *args) -> GuardrailResult: ...
@dataclass
class GuardrailPipeline:
"""Run checks in order; block on the first failure."""
input_guardrails: list[Guardrail] = field(default_factory=list)
output_guardrails: list[Guardrail] = field(default_factory=list)
tool_guardrails: list[Guardrail] = field(default_factory=list)
@staticmethod
def _run(guardrails: list[Guardrail], *args) -> GuardrailResult:
for g in guardrails:
result = g.check(*args)
if not result.passed:
return result
return GuardrailResult(passed=True)
def check_input(self, user_input: str) -> GuardrailResult:
return self._run(self.input_guardrails, user_input)
def check_output(self, model_output: str) -> GuardrailResult:
return self._run(self.output_guardrails, model_output)
def check_tool_call(self, tool: str, params: dict) -> GuardrailResult:
return self._run(self.tool_guardrails, tool, params)
Order the checks cheapest first, and decide per guardrail whether a failure should block, redact and continue, or escalate to a human. Blocking everything produces an agent nobody uses; log every decision so you can tune false positives.
3.2 Execution Sandbox¶
Agents that run code or shell commands need isolation (what can this process touch?) and resource control (how much can it consume?).
from dataclasses import dataclass, field
@dataclass
class SandboxConfig:
"""Configuration for an agent execution sandbox."""
container_image: str = "python:3.14-slim" # pin a digest in production
memory_limit_mb: int = 512
cpu_limit: float = 1.0 # cores
timeout_seconds: int = 30
network_enabled: bool = False # default deny
allowed_domains: list[str] = field(default_factory=list) # egress allowlist via proxy
read_only_paths: list[str] = field(default_factory=list)
write_paths: list[str] = field(default_factory=lambda: ["/workspace", "/tmp"])
env_vars: dict[str, str] = field(default_factory=dict) # no long-lived secrets
Sandbox strategies (weakest to strongest isolation):
| Strategy | Isolation boundary | Start-up cost | Use case |
|---|---|---|---|
| Subprocess + OS sandbox (seccomp, Landlock, macOS Seatbelt, bubblewrap) | Process with restricted syscalls and filesystem | Very low | Local coding agents on a developer's machine |
| WebAssembly | Language runtime; only the capabilities you pass in | Very low | Plugins, small pure computations |
| Container (Docker, containerd) | Linux namespaces and cgroups, shared host kernel | Low (sub-second to seconds) | Trusted or semi-trusted code, single-tenant |
| gVisor | User-space kernel intercepting syscalls | Low to medium | Untrusted code where you want container ergonomics |
| microVM (Firecracker, Kata Containers) | Hardware virtualization, separate guest kernel | Low to medium (Firecracker boots in well under a second) | Untrusted code, multi-tenant platforms |
The key interview point: a plain container shares the host kernel, so a kernel exploit escapes it. For multi-tenant execution of model-written code, use gVisor or a microVM. Also restrict network egress: without it, a hijacked agent can exfiltrate whatever it can read.
3.3 Verification Layer¶
The verification layer closes the loop by checking agent outputs before they reach the user or a production system.
class VerificationPipeline:
"""Verifies agent outputs before returning to the user (sketch)."""
def verify(self, task: "Task", output: "AgentOutput") -> "VerificationResult":
results = []
# 1. Deterministic, cheap checks first
if task.output_schema:
results.append(self._validate_schema(output, task.output_schema))
if task.has_tests:
results.append(self._run_unit_tests(output.code))
# 2. Grounding: does every claim cite a retrieved source?
if task.requires_citations:
results.append(self._check_citations(output, task.documents))
# 3. Model-based judgement last (slowest, costliest, least reliable)
if task.evaluation_rubric and all(r.passed for r in results):
results.append(self._llm_evaluate(output, task.evaluation_rubric))
return VerificationResult(passed=all(r.passed for r in results), details=results)
Verification patterns:
| Pattern | Mechanism | Best For | Watch out for |
|---|---|---|---|
| Deterministic check | Regex, schema validation, type check, diff rules | Format compliance | Only catches what you can specify |
| Runtime testing | Run the test suite in the sandbox | Code generation | Agent editing or deleting tests to make them pass; protect test files |
| LLM-as-a-judge | Separate model call scores against a rubric | Subjective quality | Position and verbosity bias, self-preference; calibrate against human labels |
| Human review | Approval gate | High-stakes or irreversible actions | Approval fatigue leads to rubber-stamping |
| A/B or shadow comparison | Run new version beside baseline | Regression detection | Needs enough traffic for significance |
Figure: cheap deterministic checks run before costly model-based judgement.
flowchart TD
A["Agent output"] --> B["Schema validation"]
B --> C["Unit tests"]
C --> D["Citation / grounding check"]
D --> E{"All passed so far?"}
E -- "yes" --> F["LLM-as-a-judge against rubric"]
E -- "no" --> G["Fail: feed back for correction"]
F --> H["Verification result"]
3.4 Context & Memory Management¶
The harness decides what the model sees on each call. The context window is a budget: more tokens cost more, add latency, and past a point make the model worse at finding what matters. Anthropic's "Effective context engineering for AI agents" calls this keeping the smallest set of high-signal tokens.
class HarnessMemory:
"""Multi-tier memory managed by the harness (sketch)."""
def __init__(self):
self.working = WorkingMemory() # current goal, plan, progress
self.conversation = SlidingWindow(max_tokens=32_000) # recent turns
self.long_term = VectorStore(collection="agent_facts")
def build_prompt_context(self, max_tokens: int = 64_000) -> str:
parts = [self.working.summarize()]
facts = self.long_term.similarity_search(self.working.current_goal, k=5)
parts.extend(f.text for f in facts)
parts.append(self.conversation.get_window())
return self._truncate_to_budget("\n".join(parts), max_tokens)
Techniques you should be able to name:
| Technique | What it does | Trade-off |
|---|---|---|
| Compaction | Summarise older history when nearing the limit | Lossy; details the summary drops are gone |
| Tool-result clearing | Drop old, bulky tool outputs and keep the call record | Model may re-fetch what it needs |
| Just-in-time retrieval | Keep file paths/IDs in context and let the agent read on demand | More tool calls, much smaller context |
| External progress notes | Agent writes a progress file / to-do list it re-reads | Survives compaction and restarts |
| Subagents | Delegate a search to a fresh context and get back a short summary | Extra cost; the parent loses the raw detail |
On the Claude API, compaction and context editing (clearing old tool results) are available as server-side features; check the current docs for model support and beta status. Also keep the stable part of the prompt (system prompt, tool definitions) byte-identical across calls so prompt caching can reuse it: caching is a prefix match, so anything volatile (timestamps, request IDs) belongs after the cached prefix.
3.5 Prompt Injection Containment¶
Crisp answer: prompt injection is not reliably preventable today, because the model reads instructions and data in the same channel. So design the harness so that a fully hijacked model still can't do serious damage.
The dangerous combination (Simon Willison calls it the lethal trifecta) is an agent that has all three of:
- access to private data,
- exposure to untrusted content (web pages, emails, issues, retrieved docs, tool results), and
- a way to send data out (HTTP requests, email, creating a public PR, even rendering an image URL).
Remove at least one leg for any given task. Concretely:
- Least privilege per task: tools and credentials scoped to what this task needs, granted for this session only.
- Egress control: default-deny network, allowlisted domains via a proxy.
- Human approval for irreversible or outbound actions, showing the exact parameters.
- Privilege separation: a quarantined model reads untrusted content and can only return structured data to a privileged planner that never sees the raw text (the "dual LLM" pattern; Google DeepMind's CaMeL formalises it with capability tracking).
- Authorize as the user: the agent's tool calls run with the end user's permissions, so injection can't escalate beyond what that user could do anyway.
- Classifiers (input screening) as one layer, measured for false negatives, never as the only one.
Figure: the lethal trifecta; remove at least one leg per task.
flowchart TD
A["Private data access"] --> T{"All three present?"}
B["Untrusted content exposure"] --> T
C["Outbound channel"] --> T
T -- "yes" --> R["Exploitable: hijacked model can exfiltrate"]
T -- "no, one leg removed" --> S["Bounded damage"]
3.6 Observability¶
Log every model call and tool call as a span in one trace per task: inputs (or hashes, if sensitive), outputs, tokens in/out, cache hits, latency, cost, the guardrail decisions and the exit reason. OpenTelemetry's GenAI semantic conventions give you standard attribute names. Without traces you can't answer the questions you'll be asked in an incident: what did the agent see, what did it do, and why did it stop?
4. HARNESS ENGINEERING BEST PRACTICES¶
4.1 Defense in Depth¶
Never rely on a single guardrail. Layer them so each catches what the previous one missed:
Input layer → Model layer → Tool layer → Output layer
──────────────────────────────────────────────────────────────────────────
Injection → System prompt → Schema → PII redaction
screening + constraints + validation + Content filter
+ Scope filter + Budget tracking + AuthZ as user + Grounding check
+ Sandbox + egress + Schema check
+ Approval gates
The tool layer carries the most weight, because that is where words become side effects.
Figure: layered guardrails, with the tool layer carrying the most weight.
flowchart LR
I["Input layer: injection screening, scope filter"] --> M["Model layer: system prompt, budget tracking"]
M --> T["Tool layer: schema, authz as user, sandbox, approval"]
T --> O["Output layer: PII redaction, grounding, schema check"]
4.2 Least Privilege for Tools¶
Every tool should have the minimum permissions needed:
TOOL_REGISTRY = {
"query_read_replica": ToolSpec(
requires_approval=False,
credentials="readonly_db_user",
allowed_databases=["analytics", "reporting"],
rate_limit_rps=100,
),
"execute_sql_write": ToolSpec(
requires_approval=True,
credentials="write_user_with_restrictions",
allowed_tables=["staging.*"],
rate_limit_rps=10,
),
}
Prefer narrow, purpose-built tools (refund_order(order_id, amount) with server-side limits) over general ones (run_sql(query)): the narrow tool encodes the policy in code, so the model can't talk its way past it. Design write tools to be idempotent (accept a client-generated request ID), because loops retry.
4.3 Harness as a Cybernetic System¶
The harness operates as a control loop with feedforward guides and feedback sensors:
Feedforward (before action):
System prompt ──→ Repo instructions / skills ──→ Plan ──→ Action
│
▼
Feedback (after action):
Action result ──→ Sensors (tests, linters, judge) ──→ Error analysis ──→ Correction
| Element | Harness Component | Example |
|---|---|---|
| Reference | Goal / task spec | "Add pagination to /orders; existing tests must pass" |
| Sensor | Verification layer | Test results, linter output |
| Comparator | Rubric or assertion | "Do all tests pass and does the diff touch only allowed files?" |
| Effector | Correction mechanism | Feed the failing test output back and retry |
A practical rule from teams running coding agents at scale: when the agent makes the same mistake twice, fix the harness (add a lint rule, a test, a line in AGENTS.md/CLAUDE.md), not just the one output.
Figure: the harness as a control loop with reference, sensor, comparator and effector.
flowchart LR
R["Reference: task spec"] --> A["Action"]
A --> S["Sensor: tests, linters"]
S --> C{"Comparator: rubric met?"}
C -- "no" --> E["Effector: feed errors back, retry"]
E --> A
C -- "yes" --> D["Done"]
4.4 Cost & Resource Governors¶
Prevent runaway agents with hard limits enforced outside the model:
from dataclasses import dataclass
@dataclass
class HarnessBudget:
"""Resource budget enforced by the harness (illustrative values)."""
max_tokens_per_session: int = 100_000
max_tool_calls: int = 50
max_wall_time_seconds: int = 300
max_cost_usd: float = 0.50
max_iterations: int = 25
Use soft limits (warn, tell the model how much budget remains so it can wrap up) before hard limits (stop and return partial results with a clear exit reason). Compute cost from the token usage the API returns for every call, including cache reads and writes, at the provider's current price list; don't hard-code prices.
4.5 Common Failure Modes¶
| Failure | Symptom | Harness fix |
|---|---|---|
| Context rot | Agent forgets constraints late in a long task | Compaction, progress notes, re-inject key constraints |
| Reward hacking | Tests "pass" because the agent weakened them | Read-only test files, diff rules, review gate |
| Tool thrash | Same failing call repeated | Repetition detector, circuit breaker |
| Over-blocking guardrails | Users route around the agent | Measure false-positive rate, tune thresholds |
| Silent cost blow-up | Bill spikes from one tenant | Per-tenant budgets and alerts on cost per task |
| Prompt injection | Agent follows instructions found in a web page | Section 3.5: remove a leg of the trifecta |
5. HARNESS ENGINEERING INTERVIEW QUESTIONS¶
| Question | Key Topics | Evaluation Rubric |
|---|---|---|
| "How would you design a safety harness for an autonomous coding agent?" | Sandboxing, egress control, least privilege, approval gates | Defense in depth, with the sandbox and permissions doing the real work rather than the prompt |
| "How do you evaluate whether your AI agent is production-ready?" | Eval harness, task sets from real traffic, repeated trials, canary | Offline eval (pass rate with variance) and online eval (shadow mode, canary, user feedback) |
| "Design a verification system for a code-generation agent." | Test execution, static analysis, diff rules, sandbox | Discusses flaky tests, protecting tests from edits, and false-positive management |
| "How would you prevent an agent from exceeding its budget?" | Token/step/dollar budgets, circuit breaker, cost tracking | Hard caps (kill switch) and soft caps (warn the model, wrap up) |
| "How do you defend an agent against prompt injection?" | Lethal trifecta, privilege separation, egress, approvals | States that filters alone fail; designs for a compromised model |
| "Compare evaluation harness vs agent harness." | Purpose, lifecycle, metrics | Measurement (eval) vs guidance and control at runtime (agent harness) |
What they probe next: "Your judge model and your agent are the same model; is that a problem?" (self-preference bias, so use a different model or calibrate against human labels). "How do you roll out a harness change?" (version prompts and tool sets like code, run the eval suite in CI, canary). "Who is accountable when the agent does something wrong?" (audit trail tying each action to the user and the approval).
6. PRODUCTION HARNESS CHECKLIST¶
| Component | Implementation | Status |
|---|---|---|
| Input guardrails | Injection screening, scope filter | 📋 |
| Output guardrails | PII redaction, content filter, schema validation | 📋 |
| Execution sandbox | gVisor/microVM for untrusted code, resource limits, egress allowlist | 📋 |
| Verification layer | Tests, linters, LLM-as-a-judge, diff rules | 📋 |
| Human-in-the-loop | Approval gates for irreversible or outbound actions | 📋 |
| Observability | One trace per task, token/cost per span, audit trail | 📋 |
| Cost governance | Token/dollar/step budgets per session and per tenant | 📋 |
| Context management | Compaction, tool-result clearing, progress notes, prompt caching | 📋 |
| Error recovery | Retry with backoff, circuit breaker, graceful degradation | 📋 |
| Eval harness | Task suite run in CI on every prompt/tool/model change | 📋 |
Next: Loop Engineering → Designing autonomous agentic loops