Skip to content

Blog

See exactly what your AI agent didcommands, files and network

Record every command and its output in your own code. Diff the files before and after the run. Log network destinations outside the sandbox.

· The Runtime team

9 min read

Read as Markdown

Runtime (withruntime.com) keeps its own record of every sandbox. It holds each command's program, exit code and duration for 14 days, and every lifecycle event for 14 days. Its account audit log is kept for at least 400 days. The log names the sandbox when code inside one calls the API. That record is deliberately thin. It never stores a command's arguments, because arguments can carry secrets. To answer "what exactly did the agent do between 2:10 and 2:40?", you need three things that you record yourself, at a boundary the agent cannot reach. This post builds all three.

What can you find out about an agent's run, and from where?

Each question has one best source. Most of those sources sit outside the sandbox:

Question Source Kept Can the agent alter it?
Which commands ran, in full? Your wrapper around exec As long as you keep it No
What did each command print? The same wrapper As long as you keep it No
Which programs ran, and did they fail? Runtime's command record 14 days No
Which files were added, changed, removed? A manifest before and after, from your code As long as you keep it No
Which hosts did it connect to? Your own upstream proxy's log Your proxy's retention No
Which hosts was it refused? The allow list, and the error it got In your command log No
When did it start, pause, stop? Runtime's events and webhooks 14 days No
Did it change keys, rules or previews? Runtime's audit log At least 400 days No
How busy was the machine? Runtime's metrics See the guide No

The pattern matters most: nothing in the right-hand column depends on a file inside the sandbox.

Why not keep the log inside the sandbox?

The agent is root inside the sandbox. Anything written there, from shell history to an audit daemon's file, can be edited or deleted by the next command. A model that follows an injected instruction will do that. Shell history is incomplete anyway. Commands run through bash -c or a script never reach it.

Record where the agent cannot act. Your code calls exec, so your code sees every command and its output when the result returns. Runtime's host enforces the network rules, so it sees every connection. Neither one runs inside the machine that the agent controls (root inside the sandbox).

How do you record every command with its output?

Send every command through one function that writes a line to your own log before it returns the result. Add a file manifest before and after the task. Together, they form the core of a flight recorder:

TypeScriptimport { createHash } from "node:crypto";import { appendFile } from "node:fs/promises";import { Sandbox } from "withruntime";const LOG = "agent-run.jsonl";const hash = (s: string) => createHash("sha256").update(s).digest("hex").slice(0, 16);const clip = (s: string) => (s.length > 4000 ? `${s.slice(0, 2000)}\n[...]\n${s.slice(-2000)}` : s);const record = (entry: object) =>  appendFile(LOG, JSON.stringify({ at: new Date().toISOString(), ...entry }) + "\n");// Every command the agent runs goes through here, never through sbx.exec directly.export async function recordedExec(sbx: Sandbox, command: string, cwd = "/workspace") {  const started = Date.now();  const r = await sbx.exec(command, { cwd, timeoutMs: 300_000 });  await record({    sandbox: sbx.id,    kind: "command",    command,    cwd,    exit: r.exitCode,    timedOut: r.timedOut,    ms: Date.now() - started,    stdout: clip(r.stdout),    stderr: clip(r.stderr),    stdoutHash: hash(r.stdout),  });  return r;}// Every file under a folder, as path -> sha256, skipping .git and node_modules.async function manifest(sbx: Sandbox, root = "/workspace") {  const r = await sbx.exec(    "find . -xdev -type f -not -path './.git/*' -not -path '*/node_modules/*' -print0 | xargs -0 -r sha256sum",    { cwd: root, timeoutMs: 120_000 },  );  const lines = r.stdout.split("\n").filter((line) => line.length > 66);  return new Map(lines.map((line) => [line.slice(66), line.slice(0, 64)] as const));}export async function recordFileChanges(sbx: Sandbox, before: Map<string, string>) {  const after = await manifest(sbx);  const added = [...after.keys()].filter((p) => !before.has(p));  const removed = [...before.keys()].filter((p) => !after.has(p));  const changed = [...after]    .filter(([p, h]) => before.has(p) && before.get(p) !== h)    .map(([p]) => p);  await record({ sandbox: sbx.id, kind: "files", added, removed, changed });  return { added, removed, changed };}await using sbx = await Sandbox.create({ labels: { run: "audit-demo" } });const before = await manifest(sbx);await recordedExec(sbx, "echo 'first note' > notes.txt && python3 --version");console.log(await recordFileChanges(sbx, before));
Pythonimport hashlibimport jsonimport timefrom datetime import datetime, timezonefrom withruntime import SandboxLOG = "agent-run.jsonl"def record(entry: dict) -> None:    with open(LOG, "a") as f:        f.write(json.dumps({"at": datetime.now(timezone.utc).isoformat(), **entry}) + "\n")def clip(s: str) -> str:    return s if len(s) <= 4000 else s[:2000] + "\n[...]\n" + s[-2000:]def recorded_exec(sbx, command: str, cwd: str = "/workspace"):    """Every command the agent runs goes through here, never through sbx.exec directly."""    started = time.monotonic()    r = sbx.exec(command, cwd=cwd, timeout_ms=300_000)    record({"sandbox": sbx.id, "kind": "command", "command": command, "cwd": cwd,            "exit": r.exit_code, "timed_out": r.timed_out,            "ms": round((time.monotonic() - started) * 1000),            "stdout": clip(r.stdout), "stderr": clip(r.stderr),            "stdout_hash": hashlib.sha256(r.stdout.encode()).hexdigest()[:16]})    return rdef manifest(sbx, root: str = "/workspace") -> dict[str, str]:    """Every file under a folder, as path -> sha256, skipping .git and node_modules."""    r = sbx.exec("find . -xdev -type f -not -path './.git/*' -not -path '*/node_modules/*' "                 "-print0 | xargs -0 -r sha256sum", cwd=root, timeout_ms=120_000)    return {line[66:]: line[:64] for line in r.stdout.splitlines() if len(line) > 66}def record_file_changes(sbx, before: dict[str, str]) -> dict:    after = manifest(sbx)    changes = {"added": [p for p in after if p not in before],               "removed": [p for p in before if p not in after],               "changed": [p for p, h in after.items() if p in before and before[p] != h]}    record({"sandbox": sbx.id, "kind": "files", **changes})    return changeswith Sandbox.create(labels={"run": "audit-demo"}) as sbx:    before = manifest(sbx)    recorded_exec(sbx, "echo 'first note' > notes.txt && python3 --version")    print(record_file_changes(sbx, before))

Copy these choices from the code, even if you write your own:

  • Output is clipped, but hashed whole. A person reads the head and tail of each stream. If you keep the raw text elsewhere, the hash shows which full output it was.
  • The environment is never logged. Pass secrets in env. They then stay out of your log, as they stay out of Runtime's. If you need to know which variables were set, log their names.
  • One sandbox ID per line. When a run forks or uses several sandboxes, the log still reads as one timeline.

Which files did the agent change?

The manifest diff answers this for the folders you list. It compares content, not timestamps. A timestamp check with find -newer is cheaper. But root can reset a file's time with touch, and a content hash cannot be faked that way.

Two other places need a manifest, beyond the project folder. /etc shows changed system configuration, such as a new package source or a hosts file entry. /usr/local shows tools installed outside the package manager. For a live view while the agent works, use a file watch. It sends writes, renames and deletes under a folder to your webhook within seconds, and it does not keep a sandbox awake (watch files for changes). The before-and-after manifest is the record. The watch is the live feed.

Which hosts did the agent connect to?

Narrow the network first, then log it. With an allow list on the sandbox, the host refuses every connection to a host that is not on the list. The command that tried the connection fails with an error, and your command log captures that error (allow only some hosts). That covers the case where the agent tried to reach a host it should not have, with no extra system.

For a record of every destination, paid accounts can route their sandboxes' connections through their own HTTP proxy, once Runtime's rules allow it. Your proxy can log each connection's host, port, time and byte counts. It cannot read the encrypted traffic. That is usually what you want. You learn that the agent talked to api.github.com for 40 KB, but not the token it sent (your own proxy).

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.networks.upstreamProxy.set({  url: "http://egress-log.example.com:3128",  secret: "PROXY_AUTH",});console.log(await runtime.networks.upstreamProxy.get());

If the proxy is down, connections fail. They do not go around it. So the log has no gaps that an agent could slip through.

What does Runtime record without any setup?

Runtime records four things. You can read all of them from code, and the agent cannot write to any of them:

  1. Commands: each command's program name, exit code and duration, for 14 days, on the sandbox's page. A failing pytest at 2:31 appears there, even if your own log is lost.
  2. Events: every create, pause, wake and stop, with the reason a sandbox stopped. Events are kept for 14 days and sent to your webhook as they happen.
  3. The audit log: changes to keys, network rules, secrets and preview visibility, plus every sandbox created. Each entry names who made the change. The log is kept for at least 400 days. A call made by code inside a sandbox names that sandbox. So an agent that was given a key, and used it to change network rules, is on record with the machine it acted from (read the audit log).
  4. Metrics: CPU and memory over time. They show when a "quiet" agent was in fact compiling for twenty minutes (read sandbox metrics).

Your flight recorder and Runtime's record should agree. Comparing them is a cheap integrity check. A command in Runtime's record that is missing from your log means something ran outside your wrapper.

What should a person read after the run?

Give a person a one-screen summary built from the log, not the raw log. Nobody reads two hundred JSON lines after every task. Everyone reads five numbers and a list. Generate the summary at the end of each run, and attach it to the pull request or the ticket the agent worked on:

textRun 2026-10-08 14:10-14:41 UTC, sandbox 7f3a2c10, task "fix leap-year parsing"Commands: 48 (3 failed: pytest x2, pip install x1), longest 2 min 14 s (pytest)Files:    4 changed, 1 added (tests/test_leap.py), 0 removed; /etc unchangedNetwork:  pypi.org, files.pythonhosted.org; 1 refused (paste.example.com)Account:  no key, rule or preview changes

Read the refused host first. An agent that tried to reach a paste site during a bug fix was probably following text that it found in an issue or a dependency's README. That is worth a look, whatever the tests say (prompt injection to code execution). Keep the full log beside the summary for as long as you keep the change. A question about a line of code six months later is answered by the command that wrote it.

In short

  • Do not trust a log that the agent can write to. Record at your exec call and at the network edge.
  • Wrap every exec to log the full command, exit code, duration and clipped output, and keep secrets in env so they never reach the log.
  • Diff a content-hash manifest before and after each task; add /etc and /usr/local when the agent has sudo.
  • An allow list records refusals for free; your own upstream proxy records every destination.

Run it on Runtime

For an agent that mostly waits on a model, Runtime costs 42% to 88% less than fourteen other sandbox providers (compare costs). Runtime keeps each command's program, exit code and duration for 14 days and its audit log for at least 400 days, outside the reach of the code in the sandbox. Read the observability guide, then create an account at withruntime.com and run the recorder above on your agent's next task.

Keep reading.

Cut your cloud billby 75%.

No card to start. Billed by the second after.

Start free, no card