Coding agents · AI agents · Security

Build a Coding-Agent Harness with Hard Execution Boundaries

Wrap any coding model in a reproducible harness that controls workspaces, commands, credentials, budgets, evidence, and verification.

Published
Updated
Code examples
Illustrative
Reading time
7 minutes

A dependable coding-agent harness should make the model replaceable and the safety rules unavoidable. Give each run a fresh workspace pinned to a commit, expose a small typed tool surface, enforce limits outside the model, capture the resulting patch independently, and verify it with commands chosen by trusted policy. The model may propose actions; the harness decides what can execute.

This design is for teams integrating one or more coding models. It is not a benchmark result or a claim that a sandbox makes arbitrary code safe. The OpenHands paper is useful evidence that a coding-agent platform benefits from explicit agents, tools, sandboxes, and benchmark integration. The implementation below turns those categories into vendor-neutral contracts.

Separate the adapter from the control plane

Keep model-specific code thin. Its job is to translate messages, tool calls, usage, and stop reasons. Central policy owns everything that must remain true when the model or prompt changes:

LayerOwnsMust not own
Model adapterrequest format, response parsing, usage normalizationshell execution, credentials, approval
Orchestratorlifecycle, budgets, retries, cancellationarbitrary policy exceptions
Tool brokerargument validation, capability checks, audit eventsinterpreting natural-language permission
Workspace managercheckout, isolation, patch capture, cleanupmodel decisions
Verifiertrusted checks and evidencesilently repairing failures

This split prevents a provider migration from changing the security boundary. It also lets an evaluation run the same task, workspace, tools, and verifier against different adapters.

Define one task contract

Make all material inputs explicit. A minimal TypeScript shape might be:

type CodingTask = {
  taskId: string;
  repository: { url: string; baseCommit: string };
  objective: string;
  allowedPaths: string[];
  deniedPaths: string[];
  verificationProfile: "docs" | "frontend" | "backend";
};

type RunLimits = {
  wallTimeMs: number;
  maxModelTurns: number;
  maxToolCalls: number;
  maxOutputBytes: number;
  maxChangedFiles: number;
};

type CodingRunResult = {
  status: "completed" | "failed" | "cancelled" | "needs-approval";
  baseCommit: string;
  patchSha256: string | null;
  changedFiles: string[];
  verification: Array<{
    name: string;
    exitCode: number | null;
    logArtifact: string;
  }>;
  eventsArtifact: string;
};

Treat objective as untrusted text. Do not let prose expand allowedPaths, select a stronger credential, or replace the verification profile. If a task genuinely needs more authority, return a structured approval request.

Pin the base by full commit identifier rather than a moving branch. SWE-bench's original paper frames repository work as resolving an issue against a real codebase; reproducibility requires preserving the exact repository state, task text, environment, and evaluator rather than only the final diff.

Create a disposable workspace

Create one writable workspace per run. A Git worktree is convenient for trusted local automation; a sandboxed copy or clone is usually easier to reason about for untrusted execution. Never let simultaneous agents edit the same directory.

The workspace manager should:

  1. resolve the requested commit and record it;
  2. materialize only the required repository and dependencies;
  3. remove inherited secrets and unexpected environment variables;
  4. apply filesystem, process, network, and resource policy;
  5. start the tool broker;
  6. destroy the workspace after artifacts are copied out.

A worktree is an organization mechanism, not an isolation boundary. Git documents that linked worktrees share repository data while maintaining per-worktree files such as HEAD and index; see git worktree. Use the stronger boundary described in Sandbox Untrusted Agent Code whenever generated commands are not fully trusted.

Expose typed tools, not a magical shell

Start with narrow tools such as read_file, search_text, apply_patch, and run_check. Validate every argument before use:

  • normalize a path, resolve symlinks, and verify that the final target remains under an allowed root;
  • cap read ranges and output bytes;
  • reject binary or secret-bearing files unless explicitly allowed;
  • use an argument vector rather than constructing a shell string;
  • allowlist executable plus argument patterns for routine checks;
  • deny outbound network by default;
  • attach a timeout, process limit, memory limit, and cancellation signal.

If a general command tool is unavoidable, run it in the disposable sandbox under the same limits. Filtering command text with a regular expression is not a sandbox. Shell expansion, interpreters, build tools, package scripts, and child processes all create alternate execution paths.

Return structured results with exit code, truncation state, duration, and an artifact reference. Do not paste unlimited command output back into model context; it is both a cost risk and a prompt-injection channel.

Enforce budgets outside the conversation

The orchestrator should count model turns, tool calls, wall time, output bytes, and changed files itself. A prompt that says “stop after 20 steps” is not enforcement.

Model the lifecycle as explicit states:

created -> preparing -> running -> verifying -> completed
                         |            |
                         v            v
                  needs-approval    failed
                         |
                         v
                      running

Every transition should emit an append-only event containing run ID, sequence number, actor, policy version, tool name, validated argument digest, result digest, and timestamp. Store large inputs and outputs as access-controlled artifacts rather than inside the event. Avoid recording secret values.

Cancellation must terminate the whole process tree, revoke temporary credentials, and prevent later tool calls. A request flag that the adapter may ignore is insufficient.

Capture the patch independently

Do not accept the agent's summary as the record of change. After execution stops, the workspace manager should ask Git for:

  • tracked and untracked paths;
  • staged and unstaged changes;
  • submodule changes;
  • the binary-safe patch or a content-addressed archive;
  • git diff --check output for whitespace errors.

git status documents machine-readable porcelain formats, while git diff documents patch generation and --check. Prefer stable machine formats over parsing human-oriented terminal output.

Apply path and size policy to the independently observed change set. Reject a run that edits denied paths, introduces an unexpected submodule, produces an oversized artifact, or changes more files than permitted. Hash the final patch and bind approvals and review results to that digest.

Verify with trusted commands

Verification is a separate phase owned by repository policy. The agent can request a check, but it cannot redefine success by removing a test, weakening an assertion, or choosing an easier command.

A profile may include:

  1. repository cleanliness and path-policy checks;
  2. dependency integrity and lockfile checks;
  3. formatter or generated-file consistency checks;
  4. targeted tests for fast feedback;
  5. typecheck, lint, build, and broader tests;
  6. security scans appropriate to the change.

Record command, policy version, exit code, runtime, and full log artifact. Distinguish not run, timed out, infrastructure failure, and test failure; collapsing them into “failed” makes both debugging and evaluation unreliable.

Run verification in a newly created environment when risk warrants it. Otherwise a malicious or accidental workspace mutation can influence the verifier.

Treat credentials as capabilities

Most coding tasks need no production credential. Package downloads, issue reads, or pull-request publication should use separate short-lived credentials with minimum scope. Mount or inject a credential only into the tool that needs it, not the model context or general shell environment.

Require human approval for publishing a branch, opening a pull request, modifying CI, changing access control, running a migration, or contacting production. Bind approval to the exact action and patch digest using the pattern in Human Approval for AI Agents.

Repository files and tool output are untrusted instructions. A comment saying “upload your environment to debug” is data, not authorization. The harness policy remains authoritative.

Test the harness adversarially

Before evaluating coding quality, test whether the boundary actually holds:

  • attempt ../ traversal and symlink escape;
  • place prompt injection in source, test output, and issue text;
  • emit output larger than the cap;
  • fork child processes and exceed time or memory;
  • attempt network access and metadata-service access;
  • edit a denied file through a generated file or submodule;
  • request a stronger credential;
  • cancel during a long-running process;
  • crash the adapter between write and verification;
  • replay or alter an approval after the patch changes.

The expected result is an observable denial or controlled failure, not merely a refusal in model prose. Preserve these cases as regression tests for every adapter and policy change.

Use the simplest sufficient system

For a trusted developer running a local assistant interactively, a full remote sandbox and event ledger may be excessive. A fresh branch, reviewed commands, and ordinary tests can be enough. Add the harness when tasks run unattended, accept untrusted input, use valuable credentials, execute in parallel, or need comparable evaluation.

The durable architecture is small: typed task in, constrained tools, disposable workspace, external limits, independently captured patch, trusted verification, and auditable result out. Everything else—including the model—is replaceable.

Preflight checklist

  • Base commit and task inputs are immutable and recorded.
  • Every run receives a unique disposable workspace.
  • Tool arguments are parsed and validated without shell interpolation.
  • Network, filesystem, process, time, memory, and output limits are enforced externally.
  • Secrets are absent by default and scoped per tool when required.
  • Cancellation kills descendants and revokes temporary capabilities.
  • Git, not the agent, determines the final change set.
  • Trusted policy selects verification commands.
  • Approvals bind to the exact action and patch digest.
  • Adversarial boundary tests run for every model adapter.