Skip to content
Level 5 · Agentic EngineerChallengePart 25 · page 10 of 1145 minSXMN 8 GB
45Minutes
2Tools
10Sources
All fourTracks
Tools used on this page2

Challenge: The Agent That Escaped the Sandbox

Validated on: written from the documentation cited above; not yet validated on hardware on any track. Per-track reproductions and the engine versions they were run with belong here once the validation pass has run this page on real machines.

Somebody says: “the agent was in a container, and it changed a file in my home directory.” By the end of this page you will have a procedure that finds out how, in about five minutes, and you will have run it against a fault you introduced deliberately so that you recognise the evidence when the fault is not yours.

The deliverable is a written diagnosis: the evidence you collected, the fault it pointed at, the change you made, and the boundary test passing afterwards. That shape — evidence, then a named fault, then proof — is what separates closing a hole from moving a symptom.

An agent went somewhere it should not have: what to do, in order

  1. Preserve the evidenceDo not restart the container. A stopped container tells you much less, and a recreated one tells you nothing about the run that misbehaved.
  2. Find out what it could seeThe mount list, from the host. Almost every escape is a mount somebody added for convenience and forgot.
  3. Find out what it was handedThe environment. A key in the environment is available to every command the agent runs, including ones you did not anticipate.
  4. Find out where it could reachThe network mode and whether the network is internal. An agent with a route off the machine can exfiltrate what it read.
  5. Check for the exitsA container socket, host network mode, privileged mode, root. Each one makes the container boundary decorative.
  6. Ask what the agent was allowed to doThe permission mode. This does not create an escape, but it decides whether a hole was walked through.
  7. Fix exactly one thing, then re-testThe boundary test from the previous lab, with the check that was failing now passing, and the positive control still passing.
The order is not arbitrary. Each step is cheap, and the first two settle most cases outright, because most escapes are a mount rather than anything clever.

Almost every “the agent escaped” story is one of these five. Learn the list and you have learned the diagnosis.

Fault What the evidence looks like
A mounted home directory The mount list contains the user’s home, often read-only, often added “so it can see my dotfiles”. Every key under it was readable.
The container socket inside the container A mount of docker.sock or podman.sock. The container can start containers, including one that mounts the host root.
Secrets in the environment Credential-shaped variables in the container’s environment, usually inherited from the shell that started it or from an env_file that was wider than intended.
Network policy never applied The container is on an ordinary bridge network rather than an internal one, or in host network mode. Nothing failed; the policy was simply never there.
An auto-approve permission mode The agent was set to approve everything, so a hole that a prompt would have surfaced was walked straight through without anybody seeing it.

Notice what is not on that list: the model, the prompt and the agent’s own quality. Those change what an agent tries. They do not change what it is able to do, and the difference between those two sentences is the whole subject of this page.

The sandbox from the previous lab, its boundary test, and a container engine. Forty-five minutes, all of it attended. No downloads and no model larger than whatever you already run.

Track S — NVIDIA DGX Spark

Docker Engine with the Compose plugin. Every fault below reproduces as written. Reproduce the container-socket fault last, and remove the mount as soon as you have seen the evidence.

Track X — AMD Ryzen AI Max+ 395

Docker Engine or Podman. Under rootless Podman the socket fault is less severe by construction, because, as the rootless tutorial puts it, Podman “is not, and will never be, root”. Reproduce it anyway and compare what the audit reports in each case; the difference is the most useful thing this track can show you.

Track M — Apple siliconPartial

The container faults reproduce through Docker Desktop. On the dedicated-user route there is no container to audit, so the equivalent investigation is a filesystem-permission and environment audit of that account.

With Docker Desktop, all five faults reproduce as written.

On the dedicated-user route, three of the five still apply in a different form and are worth working through by hand: a home directory the agent’s account can read, credential-shaped variables in that account’s shell profile, and an auto-approve permission mode. The audit script inspects containers, so use ls -l, env and the agent’s own configuration instead, and record the same three columns: evidence, fault, fix.

Track N — NVIDIA desktop or laptop

Docker Engine or Podman. Every fault reproduces as written. If you run inside WSL2, note that the Windows filesystem is mounted inside the Linux environment, so “the project directory” and “a Windows path” can be the same mount, and a bind mount of a WSL2 path can reach further than you expect. Check the mount list rather than assuming.

Working directory and terminal roles

Prepare the course execution workspace once before this procedure. It includes this part's scripts, data and shared Python helpers. In the client or training terminal, select this directory:

RunnableAll tracks

select this part’s execution directory
export LABS_ROOT="${LABS_ROOT:-$HOME/llm-course/labs}"
export LAB_DIR="$LABS_ROOT/part-25-coding-agents"
cd "$LAB_DIR"
pwd
test -f "audit-sandbox.sh"

Expected result: pwd ends in part-25-coding-agents and the file check returns successfully. If it does not, finish workspace preparation before continuing. Activate the environment in the requirements for your track. Bare script and data filenames below are relative to this directory; paths to earlier experiments must point at the artefacts you actually retained.

Keep each foreground server in a separate terminal and send requests from this terminal. Reapply lesson-specific environment variables in each new shell. Stop at the first failed checkpoint and retain its output; the execution guide explains how to distinguish missing files, endpoint failures and capacity problems.

1. Take the audit script and read a healthy container first

Section titled “1. Take the audit script and read a healthy container first”

RunnableAll tracks

audit-sandbox.sh
#!/usr/bin/env bash
# Purpose: audit a running agent container from the host - what it has mounted, what is in
# its environment, which networks it is attached to, whether a container socket is
# exposed, and which privilege settings are in force - and name each finding as
# one of the faults from this part's challenge.
# Platform: spark, strix, nvidia (Docker Engine or Podman); mac through Docker Desktop
# Minimum memory: 8 GB for the stack being audited; this script needs almost nothing
# Assumes: run on the HOST, not inside the container, with docker (or podman, through
# `alias docker=podman`) and python3 on PATH, and the container running. It only
# reads: `docker inspect` changes nothing.
#
# Usage:
# ./audit-sandbox.sh agent-sandbox-agent-run-1
# ENGINE=podman ./audit-sandbox.sh my-agent
# LABBOOK=~/agent-lab/labbook.md ./audit-sandbox.sh my-agent
set -euo pipefail
CONTAINER="${1:-}"
ENGINE="${ENGINE:-docker}"
LABBOOK="${LABBOOK:-$PWD/labbook.md}"
PROJECT_DIR="${PROJECT_DIR:-}"
if [ -z "$CONTAINER" ]; then
echo "usage: $0 <container-name-or-id>" >&2
echo " list running containers with: $ENGINE ps" >&2
exit 2
fi
for required in "$ENGINE" python3; do
if ! command -v "$required" >/dev/null 2>&1; then
echo "$required is required and was not found on PATH." >&2
exit 1
fi
done
if ! "$ENGINE" inspect "$CONTAINER" >/dev/null 2>&1; then
echo "No such container: $CONTAINER" >&2
exit 1
fi
INSPECT_JSON="$("$ENGINE" inspect "$CONTAINER")"
# A container's own inspect output does not report whether the networks it is attached to
# are internal; that lives on the network objects. Collect them all and let the report
# match them by name. If this fails, the report says the flag was not available rather
# than assuming either answer.
NETWORKS_JSON="[]"
if network_names="$("$ENGINE" network ls --format '{{.Name}}' 2>/dev/null)"; then
# shellcheck disable=SC2086
NETWORKS_JSON="$("$ENGINE" network inspect $network_names 2>/dev/null || echo '[]')"
fi
CONTAINER="$CONTAINER" PROJECT_DIR="$PROJECT_DIR" LABBOOK="$LABBOOK" \
INSPECT_JSON="$INSPECT_JSON" NETWORKS_JSON="$NETWORKS_JSON" python3 - <<'PY'
"""Read one container's inspect output and name every fault it finds."""
import datetime
import json
import os
import sys
data = json.loads(os.environ["INSPECT_JSON"])
if not data:
print("inspect returned nothing")
raise SystemExit(1)
container = data[0]
name = container.get("Name", os.environ["CONTAINER"]).lstrip("/")
config = container.get("Config") or {}
host_config = container.get("HostConfig") or {}
network_settings = container.get("NetworkSettings") or {}
mounts = container.get("Mounts") or []
project_dir = os.environ.get("PROJECT_DIR") or ""
findings = []
def fault(code, detail):
findings.append({"fault": code, "detail": detail})
print(f"Sandbox audit: {name}")
print("=" * (16 + len(name)))
print()
# ---------------------------------------------------------------- 1. what it can see
print("Mounts")
socket_names = ("docker.sock", "podman.sock", "containerd.sock")
sensitive_roots = ("/etc", "/var", "/usr", "/root", "/boot", "/proc", "/sys")
if not mounts:
print(" (none)")
for mount in mounts:
source = mount.get("Source", "?")
destination = mount.get("Destination", "?")
mode = "rw" if mount.get("RW", True) else "ro"
print(f" {mode} {source} -> {destination}")
if any(token in source for token in socket_names):
fault(
"container-socket-mounted",
f"{source} is a container socket. A process that can reach it can start a "
"container that mounts the host root, so this container is not a boundary.",
)
continue
trimmed = source.rstrip("/")
if source == "/":
fault("host-root-mounted", "the host root filesystem is mounted into the container")
continue
looks_like_home = (
trimmed.startswith("/home/") or trimmed.startswith("/Users/")
) and trimmed.count("/") == 2
if looks_like_home:
fault(
"home-directory-mounted",
f"{source} is a home directory. Read-only does not help: keys, tokens and "
"every private repository under it are readable by the agent.",
)
continue
system_mount = False
for root in sensitive_roots:
if trimmed == root or trimmed.startswith(root + "/"):
fault(
"system-directory-mounted",
f"{source} is a system directory and has no business inside an agent sandbox",
)
system_mount = True
break
if system_mount:
continue
if mode == "rw" and project_dir and os.path.realpath(source) != os.path.realpath(project_dir):
fault(
"extra-writable-mount",
f"{source} is writable and is not the project directory you named",
)
print()
# ------------------------------------------------------------ 2. what it was handed
print("Environment")
env_pairs = [entry.split("=", 1) for entry in (config.get("Env") or []) if "=" in entry]
secretish = ("TOKEN", "SECRET", "PASSWORD", "PASSWD", "API_KEY", "APIKEY", "CREDENTIAL")
allowed = {"OPENAI_API_KEY", "AGENT_GATEWAY_KEY", "ANTHROPIC_AUTH_TOKEN"}
env_findings = 0
for key, value in env_pairs:
upper = key.upper()
if any(token in upper for token in secretish):
shown = "set" if value else "empty"
marker = " " if key in allowed else "! "
print(f" {marker}{key} = <{shown}>")
if key not in allowed:
env_findings += 1
fault(
"secret-in-environment",
f"{key} was passed into the container. An agent inherits its environment, "
"and anything in it is available to every command the agent runs.",
)
if env_findings == 0:
print(" no unexpected credential-shaped variables")
print(f" ({len(env_pairs)} variable(s) in total)")
print()
# ------------------------------------------------------------- 3. where it can reach
print("Network")
network_mode = host_config.get("NetworkMode", "default")
print(f" mode: {network_mode}")
if network_mode == "host":
fault(
"host-network-mode",
"the container shares the host network namespace, so every service bound to "
"the host loopback address is reachable from inside it",
)
try:
all_networks = json.loads(os.environ.get("NETWORKS_JSON") or "[]")
except json.JSONDecodeError:
all_networks = []
internal_by_name = {
net.get("Name"): net.get("Internal")
for net in all_networks
if isinstance(net, dict)
}
networks = network_settings.get("Networks") or {}
if not networks and network_mode not in ("none", "host"):
print(" (no attached networks reported)")
for net_name, net in networks.items():
internal = net.get("Internal")
if internal is None:
internal = internal_by_name.get(net_name)
if internal is None:
label = "internal flag not available; check it with `network inspect` by hand"
else:
label = "internal" if internal else "externally connected"
print(f" {net_name}: {label}")
if internal is False:
fault(
"network-policy-not-applied",
f"network {net_name} is not internal, so the agent has a route off this machine",
)
if network_mode == "none":
print(" no network at all; an agent here cannot reach a model either")
print()
# --------------------------------------------------------------- 4. what it may do
print("Privileges")
user = config.get("User") or "(image default, probably root)"
print(f" user: {user}")
if not config.get("User") or config.get("User").split(":")[0] in ("0", "root"):
fault("runs-as-root", "the container process runs as root inside the container")
if host_config.get("Privileged"):
fault("privileged-container", "the container is privileged, which removes almost every control")
print(f" privileged: {bool(host_config.get('Privileged'))}")
readonly_rootfs = bool(host_config.get("ReadonlyRootfs"))
print(f" read-only root filesystem: {readonly_rootfs}")
if not readonly_rootfs:
fault(
"writable-root-filesystem",
"the root filesystem is writable, so the agent can install tools and leave "
"things behind outside the project",
)
cap_add = host_config.get("CapAdd") or []
if cap_add:
print(f" added capabilities: {', '.join(cap_add)}")
fault("capabilities-added", f"capabilities added: {', '.join(cap_add)}")
security_opt = host_config.get("SecurityOpt") or []
print(f" security options: {', '.join(security_opt) if security_opt else '(none)'}")
if not any("no-new-privileges" in str(option) for option in security_opt):
fault(
"new-privileges-allowed",
"no-new-privileges is not set, so setuid binaries inside the image can still "
"raise privileges",
)
memory = host_config.get("Memory") or 0
pids = host_config.get("PidsLimit") or 0
print(f" memory limit: {'unlimited' if not memory else str(memory) + ' bytes'}")
print(f" pids limit: {'unlimited' if not pids else pids}")
if not memory:
fault(
"no-resource-limits",
"no memory limit: a runaway agent can make the whole machine unusable. This is "
"not a security boundary, only a usability one.",
)
print()
# ------------------------------------------------------------------------- verdict
print("Findings")
if not findings:
print(" none. Every check this script performs is satisfied.")
else:
seen = set()
for item in findings:
key = (item["fault"], item["detail"])
if key in seen:
continue
seen.add(key)
print(f" [{item['fault']}] {item['detail']}")
print()
print(f"{len(findings)} finding(s).")
record = {
"lab": "part-25-agent-that-escaped",
"recorded": datetime.datetime.now(datetime.timezone.utc)
.replace(microsecond=0)
.isoformat(),
"container": name,
"findings": [item["fault"] for item in findings],
"clean": not findings,
}
try:
with open(os.environ["LABBOOK"], "a", encoding="utf-8") as handle:
handle.write(json.dumps(record) + "\n")
print("recorded in " + os.environ["LABBOOK"])
except OSError as error:
print("could not write the lab notebook: " + str(error))
sys.exit(1 if findings else 0)
PY

Download audit-sandbox.sh285 lines

It runs on the host, reads only, and reports four things: what the container has mounted, what credential-shaped variables are in its environment, which networks it is on and whether they are internal, and which privilege settings are in force. Every finding it prints is named after one of the faults in the table above.

Run it against the sandbox from the previous lab while that sandbox is still correct. Knowing what a clean report looks like is what makes a dirty one legible.

RunnableAll tracks

the healthy baseline
cd ~/agent-lab
docker compose -f compose-sandbox.yaml up -d agent
bash audit-sandbox.sh agent-sandbox-agent-1

Output — what you should see

Sandbox audit: agent-sandbox-agent-1
====================================
Mounts
rw /home/user/agent-lab/task-work -> /work
ro /home/user/agent-lab/task -> /task
Environment
no unexpected credential-shaped variables
(1 variable(s) in total)
Network
mode: agent-sandbox_sandbox
agent-sandbox_sandbox: internal
Privileges
user: 10001:10001
privileged: False
read-only root filesystem: True
security options: no-new-privileges=true
memory limit: 4294967296 bytes
pids limit: 512
Findings
none. Every check this script performs is satisfied.
0 finding(s).

The container name comes from your Compose project; docker compose ps tells you what it is.

The commonest fault, and the one that feels most harmless while you are adding it.

Fragment — not complete on its own

Terminal window
docker compose -f compose-sandbox.yaml run --rm \
-v "$HOME:/host-home:ro" \
--name leaky-home \
agent sleep 600

Audit it from another terminal, and the report names it: a home directory is mounted, and read-only does not help.

Why it is fatal. An agent that can read a home directory can read every SSH key, every cloud credential file, every browser profile and every private repository under it. Docker’s own bind-mount documentation frames the general property: a bind mount gives a container access to host files, and the ro option only removes the write half of that. The read half is how credentials leave.

How it happens. Somebody wanted the agent to see a dotfile, a shared configuration or a second repository, and mounting the home directory was one line instead of three.

The fix. Mount the specific thing. If the agent needs one configuration file, mount that file. If it needs a second repository, mount that repository. The rule is that every entry in the mount list is a decision somebody can defend.

The proof. Re-run boundary-test.sh inside the container: host-home-not-mounted and planted-secret-unreadable go from failing to passing, and gateway-reachable still passes.

3. Fault two: the container socket inside the container

Section titled “3. Fault two: the container socket inside the container”

The fault that makes every other control decorative.

Fragment — not complete on its own

Terminal window
docker compose -f compose-sandbox.yaml run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
--name leaky-socket \
agent sleep 600

Why it is fatal. Docker’s security documentation states the consequence directly: “you can start a container where the /host directory is the / directory on your host; and the container can alter your host filesystem without any restriction”. The post-installation guide adds the same point about the group: “The docker group grants root-level privileges to the user.” So a container with the socket in it can create a second container with no restrictions at all, and everything you configured on the first one is irrelevant.

How it happens. Almost always because a tool needs it. OpenHands’ published local-setup command mounts the socket, because its architecture starts containers for the agent’s work. That is a real requirement and a real trade, and it is fine on a machine where you have decided it is fine. It is not fine on a machine where you believed the outer container was a boundary.

The fix. Remove the mount. If the tool genuinely needs to start containers, accept that the outer container is not the boundary and put the boundary somewhere else: a dedicated machine, a virtual machine, or a user account that is not yours. Do not tell yourself the container is holding.

The proof. container-socket-absent passes, and the audit reports no socket mount.

4. Fault three: secrets in the environment

Section titled “4. Fault three: secrets in the environment”

Fragment — not complete on its own

Terminal window
docker compose -f compose-sandbox.yaml run --rm \
-e HF_TOKEN \
-e AWS_SECRET_ACCESS_KEY \
--name leaky-env \
agent sleep 600

Those two flags pass the variables through from your shell without naming their values, which is what makes this fault so easy to introduce: the command looks like it is not carrying a secret.

Why it is fatal. An agent inherits its environment, and so does every command the agent runs. A token in the environment is available to a shell command the model decided to run, to a test suite, to a build script and to anything any of those invoke. It is also the one class of secret that a read-only filesystem and a careful mount list do nothing about.

How it happens. An env_file that was written for the whole project rather than for the sandbox, a -e VAR added while debugging, or a shell profile that exports credentials and a Compose file that passes the environment through.

The fix. An explicit environment: block listing exactly the variables the agent needs, and a per-sandbox gateway key that is scoped to the aliases it uses and can be revoked without affecting anything else. The previous lab’s Compose file does both.

The proof. environment-is-clean passes, and the audit’s environment section names only the gateway key.

5. Fault four: network policy that was never applied

Section titled “5. Fault four: network policy that was never applied”

Fragment — not complete on its own

Terminal window
docker compose -f compose-sandbox.yaml run --rm \
--network bridge \
--name leaky-network \
agent sleep 600

Why it matters. Nothing failed here and nothing was misconfigured in the sense of throwing an error. The policy simply was not there. An agent on an ordinary bridge network has a route off the machine, which turns everything it can read into everything it can send, and turns an information-disclosure problem into an exfiltration one.

How it happens. A Compose override that reattaches the service to the default network, an external: true network that resolves to the wrong one, or a --network flag typed during debugging and never removed. It is invisible without an audit precisely because everything works.

The fix. The internal: true network from the previous lab, which the Compose reference describes as letting you “create an externally isolated network”, with the gateway attached to it so the agent can still reach a model. Where an agent needs no model at all, --network none is the strongest option and is documented as creating a container in which “only the loopback device is created”.

The proof. internet-unreachable passes and gateway-reachable still passes. Both, together. Either one alone is meaningless.

6. Fault five: an auto-approve permission mode

Section titled “6. Fault five: an auto-approve permission mode”

This one is different from the other four, and the difference is the point.

Every tool in this part has such a setting: Codex CLI’s --dangerously-bypass-approvals-and-sandbox with its --yolo alias, described in its own documentation as “No sandbox; no approvals (not recommended)”; OpenCode’s --auto, which “automatically approve[s] permission requests that are not explicitly denied”; Cline’s auto-approve list with its “Edit all files” and “Execute all commands” entries and the mode named after a slogan; Claude Code’s bypassPermissions, which --dangerously-skip-permissions selects; Goose’s automatic mode.

Why it matters, and why it is not an escape. An auto-approve mode creates no hole. What it does is remove the last chance for a human to notice one. On a correctly built sandbox it is a reasonable setting and the previous lab argued for it. On a sandbox with any of the four faults above, it is the difference between “the agent asked to read a file outside the project and I said no” and “the agent read every key in my home directory and I found out on Thursday”.

The fix. Not “turn it off”. The fix is to make the sandbox correct first and then decide the permission mode deliberately, in that order, and to write down which mode you chose beside the audit report so that the two are reviewed together.

The proof. There is no boundary-test check for this one, which is itself informative: the tools you have cannot verify a decision you made about risk. Record it in the notebook instead.

One page, for one fault of your choosing, in the shape the procedure implies.

Pseudocode — not a real command

Symptom what was observed, and where the file that changed lives
Evidence the audit output, with the finding that named the fault
Fault one of the five, named
Fix the single change made
Proof the boundary test before and after, with the control still passing
Residual what is still true that you have decided to accept, and why

The last line is the one people skip and the one that matters most six months later. A sandbox with the container socket in it because a tool needs it is a defensible position; a sandbox with the socket in it that nobody has written down is an accident waiting to be discovered by somebody else.

Diagnose the permission that enabled the escape

Section titled “Diagnose the permission that enabled the escape”

Start from the hardened lab configuration and its boundary-test results. Introduce one deliberate fault in a disposable container, using synthetic files and credentials. Record the exact mount, socket, environment value, network change or approval setting involved.

Inspect the container configuration and run the relevant direct probe before asking the agent to try it. This distinguishes excessive operating-system authority from the model’s willingness to use that authority. A denied model request is not proof that a mounted host directory is inaccessible.

Restore the boundary, recreate the container where required and repeat both the forbidden operation and an ordinary permitted task. Save the evidence showing that useful work remains possible while the escape path is closed. Avoid treating approval prompts as a replacement for isolation: a misleading prompt or auto-approved action can still exercise whatever authority the environment grants. Finish with an incident note that names the breached boundary, consequence, detection and repair. Keep real home directories, production sockets and real credentials out of the fault demonstrations; the synthetic setup is sufficient to prove the mechanism.

You are done when all of the following are true:

  • a clean audit report exists for your sandbox, with zero findings;
  • you have reproduced at least three of the five faults and can point at the line of audit output that named each one;
  • for each reproduced fault you have run the boundary test before and after the fix, and the relevant check moved from failing to passing while gateway-reachable stayed passing;
  • the container-socket fault has been reproduced, understood and removed;
  • the written diagnosis exists, including the “Residual” line;
  • labbook.md contains the audit records the script wrote, one per run;
  • you can state, in one sentence each, why a read-only home mount is still fatal and why an auto-approve mode is not itself an escape.

A five-minute procedure you can run from memory, and a calibrated sense of which faults matter most.

The rough ordering, which your own investigation will make concrete: the container socket is the worst because it removes the boundary entirely; a mounted home directory is the most common and leaks the most in practice; secrets in the environment are the easiest to introduce accidentally and the hardest to see; a missing network policy converts a disclosure into an exfiltration; and an auto-approve mode multiplies whichever of the other four you have.

The audit script reports that the internal flag is not available. Some engines do not report it on the container’s own inspect output, which is why the script also reads the network objects. If both are unavailable, inspect the network by hand and record the answer; do not assume either way.

The audit reports no findings but the agent still edited a file outside the project. Three possibilities remain. There is a Compose override you have not read, and docker compose config prints the merged configuration that actually ran. The file was inside a mounted directory you did not think of, such as a parent of the project. Or the agent was not running in the container at all, which is more common than it sounds when a tool has both a containerised and a native mode.

The boundary test passes and the audit reports findings. They test different things from different sides. The audit sees the configuration from the host; the boundary test sees the consequences from inside. A finding with no matching failure usually means a hole that exists and was not reachable by the specific probe. Fix it anyway.

On rootless Podman the socket fault looks less alarming. It is genuinely less severe, because the daemon is not root. It is still a way for the container to start containers with your user’s privileges, which includes everything your user can read. Record the difference rather than dismissing the fault.

You cannot reproduce a fault because Compose refuses the override. Use docker run directly for the reproduction. The point is the evidence in the audit report, not the orchestration.

RunnableAll tracks

stop the deliberately broken containers
docker rm -f leaky-home leaky-socket leaky-env leaky-network

Re-run the audit against your real sandbox and confirm it is back to zero findings. Delete the planted secret if you recreated it. Keep the audit reports and the diagnosis.

  • Evidence before theory, again. The mount list and the environment settle most escapes before you have formed an opinion, and both are one read-only command away.
  • Read-only is not safe for a home directory. The write half is not how credentials leave.
  • The container socket is an exit, not a weakness. A container that can reach it is not bounded by that container, and the documentation says so in as many words.
  • A policy that was never applied looks exactly like a policy that is working. Nothing errors. Only an audit or a boundary test can tell the two apart, which is why both are files you keep rather than steps you did once.
  • An auto-approve mode is a multiplier, not a hole. It removes the last human check on whatever else is wrong, which is why the order is always: build the boundary, prove it, then decide the permission mode.
  • Write down the residual risk. Every real sandbox has something you decided to accept. The difference between engineering and hoping is whether that decision is written next to the audit report.

Record in the notebook: the clean audit report, the three or more faults you reproduced with the line of output that named each, the boundary-test sheets before and after each fix, the written diagnosis with its Residual line, and the permission mode you have chosen for each agent you run.

Check your understanding

Question 1. An agent container has a read-only mount of the user's home directory. Why is that still a serious fault?
Show the answer and why

Answer: Because reading is how credentials leave: SSH keys, cloud credential files, browser profiles and private repositories under that directory are all readable by a program nobody reviewed

The write half of a bind mount is only half the exposure. This is also the commonest fault in practice, because mounting a home directory is one line and looks harmless when it carries ro.

Question 2. What is the first thing to do when told an agent modified a file outside its project?
Show the answer and why

Answer: Preserve the evidence and read the container's mount list from the host, because most escapes are a mount somebody added and forgot

Restarting destroys the state that explains what happened. The mount list is cheap, read-only and settles most cases outright, which is why it is second in the procedure and the restart is nowhere in it.

Question 3. Why does an auto-approve permission mode appear on the fault list even though it creates no hole?
Show the answer and why

Answer: Because it removes the last human check on whatever else is wrong, turning a question the agent would have asked into an action nobody saw

It is a multiplier. On a correct sandbox it is reasonable and the previous lab argued for it. On a sandbox with a mounted home directory it is the difference between a prompt you declined and a leak you found out about later.

Question 4. Which of these are true about a container with /var/run/docker.sock mounted? Select all that apply.
Show the answer and why

Answer: It can start a container that mounts the host root filesystem, Docker documents the docker group as granting root-level privileges, Some tools mount it because their architecture requires starting containers

The second is exactly the mistaken belief this fault exists to correct. Read-only root, dropped capabilities and a non-root user on the outer container do nothing about a second container created without them.

Question 5. You fix a fault and the boundary test now shows one more pass and one fewer fail, with the gateway control unchanged. What does that establish?
Show the answer and why

Answer: That the specific hole the failing check probed is closed, and that the sandbox is still a working environment rather than an empty one

A test proves what it probes and nothing more. The unchanged control matters as much as the changed check: a fix that also broke the gateway would look like progress and would have produced a container in which no agent can run.

Sources for this lesson

10 verified · checked 2026-09-09

  1. 01Docker — security§ Docker daemon attack surfacedocs.docker.com/engine/security2026-09-09
  2. 02Docker — post-installation steps for Linux§ docker group privilegesdocs.docker.com/engine/install/linux-postinstall2026-09-09
  3. 03Docker — bind mounts§ Considerations and constraintsdocs.docker.com/engine/storage/bind-mounts2026-09-09
  4. 04Docker — none network driverdocs.docker.com/engine/network/drivers/none2026-09-09
  5. 05Compose file reference — networks§ internaldocs.docker.com/reference/compose-file/networks2026-09-09
  6. 06Docker — run reference§ env; env-file; read-only; userdocs.docker.com/reference/cli/docker/container/run2026-09-09
  7. 07Codex — agent approvals and security§ Approval policies; bypass flaglearn.chatgpt.com/docs/agent-approvals-security2026-09-09
  8. 08OpenCode — permissionsopencode.ai/docs/permissions2026-09-09
  9. 09Cline — auto-approvedocs.cline.bot/features/auto-approve2026-09-09
  10. 10Claude Code — permission modescode.claude.com/docs/en/permission-modes2026-09-09

Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.