tim witter
All articles

Blog

Proportionate isolation, not safety badges

I choose isolation by asking what could go wrong, not by chasing a label.

Begin with the risk

A useful boundary depends on the material, the actions allowed, and the cost of a mistake. A read-only task with public input has different consequences from a task that can alter shared state. I first name the plausible harm and the path by which an action could cause it. Only then do I choose which resources to separate and which operations to withhold. More isolation is not automatically better if it prevents meaningful verification.

I keep a short list of harms rather than a general worry: reading material that was not meant to leave its folder, writing over shared state, reaching the network, spending a budget, publishing something unfinished. Each harm suggests a different boundary, and some suggest none at all. Naming them prevents isolation from becoming a ritual performed for its own sake.

The choice is then a matching exercise. A read-only task over public input may need nothing more than a separate working directory and a clear output path. A task that can alter shared state needs review at the point where its output leaves the boundary. The smallest separation that covers the named harm keeps the work verifiable.

isolation.jsonJSON
{
  "task": "summarize local drafts",
  "readOnly": ["/usr", "/bin", "workspace/input"],
  "writable": ["workspace/output"],
  "network": "none",
  "reviewBeforePublish": ["workspace/output"]
}

Account for what remains

Isolation can limit reach, but it does not establish that instructions are correct, outputs are accurate, or information is appropriate to publish. It can also make a test unrepresentative if the restricted environment differs from the real one in important ways. I would document those limits instead of treating a sandbox as a safety badge. Review still matters at the point where work leaves the isolated setting.

Isolation also changes what can be tested. Inside a restricted environment, the network may be gone, the user may differ, and the dependencies may be fewer than in the real one. A check that passes there answers a narrower question, and I write down the difference instead of assuming it does not matter. Otherwise the boundary quietly weakens the evidence.

It is equally important not to confuse containment with judgment. A sandbox does not review content, verify a claim, or decide whether something should be published. The review step stays where it was, and the fact that work happened in isolation should not lower the attention it receives on the way out.

boundary.test.tsTypeScript
// Both sides of the boundary are checked: a permitted read
// succeeds, and a write outside the allowlist fails loudly.
test("the boundary allows reads and refuses unlisted writes", () => {
  expect(readInside("workspace/input/draft-1.md")).toBeDefined();

  expect(() => writeOutside("/etc/hosts")).toThrow(/outside the boundary/);
});

Check the chosen boundary

I would try a permitted operation and a representative prohibited one, then inspect whether both outcomes match the intended policy. I would also verify that the useful checks still work inside the boundary, or explicitly defer them to a controlled later step. This is a tradeoff, not a certificate: the right isolation makes a particular failure less likely or less costly while leaving its uncovered risks visible.

I test both sides with representative operations: a read that should be allowed and a write to an unlisted path that should fail. I check that the failure is clear rather than silent, because a refused action with a readable reason is a boundary working as intended. I also confirm that the checks I still need can run inside, or I note that they are deferred to a later step.

run-isolated.shShell
#!/usr/bin/env bash
set -euo pipefail

# Only the workspace is writable; tools and inputs are read-only.
bwrap \
  --ro-bind /usr /usr \
  --ro-bind /bin /bin \
  --bind "$PWD/workspace" /workspace \
  --tmpfs /tmp \
  --unshare-all \
  --die-with-parent \
  --chdir /workspace \
  ./task.sh