Audit Trail
Tamper-evident NDJSON log of every event in a sandbox's lifetime. Per-tenant.
Every sandbox has a tamper-evident audit log that records lifecycle, command, file-transfer, credential-proxy, and blocked-egress events with a chained HMAC, so any change to a past entry is detectable. The chain is keyed with a per-team secret.
Read the log
import { Isorun } from 'isorun'
const isorun = new Isorun()
const sandbox = await isorun.create({ image: 'python:3.12-slim' })
try {
await sandbox.exec('echo hello')
await sandbox.exec("python3 -c 'print(2 + 2)'")
for (const entry of await sandbox.auditLog()) {
console.log(entry.timestamp, entry.event, entry.data)
}
// 2026-05-26T… pool.warm_popped { image: 'python:3.12-slim', ... }
// 2026-05-26T… sandbox.created { image: 'python:3.12-slim', vcpus: 1, ... }
// 2026-05-26T… exec.start { command: 'echo hello' }
// 2026-05-26T… exec.exit { command: 'echo hello', exit_code: 0, duration_ms: 12 }
// 2026-05-26T… exec.start { command: "python3 -c 'print(2 + 2)'" }
// 2026-05-26T… exec.exit { command: "python3 -c 'print(2 + 2)'", exit_code: 0, duration_ms: 41 }
} finally {
await sandbox.destroy()
}Each entry is a JSON object with at least:
{
"seq": 3,
"ts": "2026-05-26T14:23:01.412Z",
"sandbox_id": "run...",
"event": "exec.exit",
"data": { "command": "echo hello", "exit_code": 0 },
"hmac": "sha256:abc123..."
}Verify the chain
Each entry is chained to the previous one with a keyed hash, so tampering with any entry breaks the chain at that point and every later entry. You can verify the chain locally without round-tripping to the runner: pull your team secret from the dashboard once and run the verifier against the log.
What's logged
| Event | When | Data |
|---|---|---|
pool.warm_created | A warm-pool VM is pre-booted (its log carries into your sandbox) | image, vcpus, mem_mib |
pool.warm_popped | A warm VM is assigned to you | image, vcpus, mem_mib, pop_ms, org_id |
sandbox.created | Create returns | image, vcpus, mem_mib |
sandbox.forked | A child is forked from this run | parent, image |
exec.start | Each exec() | command |
exec.exit | Same call returns | command, exit_code, duration_ms |
file.upload | A file is written into the sandbox | path, size |
file.download | A file is read out of the sandbox | path, size |
credential.proxied | A proxied credentialed request is made | service, method, path, status (never the credential) |
network.blocked | An egress connection is denied by policy | domain or ip + port, reason |
sandbox.destroyed | destroy() or auto-destroy | cpu_ms, mem_peak_bytes, uptime_ms |
Command output (stdout/stderr) is never recorded, only the command line, its exit code, and duration. File events record path and size, not contents; credential-proxy events never include the credential.
The log is flushed to object storage (R2) on destroy (best-effort) so you have a permanent audit trail even after the runner is torn down.
Guarantees
- Append-only verification. Anyone with the team secret can verify any prefix of the log without needing the later entries, useful for streaming verification during long-running sessions.
- Per-tenant isolation. Each team has its own secret, so a leaked verification key only affects that one team's audit trail.
There is no API to mutate or delete entries. The only way to break the chain is to tamper with the stored file, which you would detect on the next verification.
The flush to object storage on destroy is best-effort. Read the log before you destroy a sandbox if you need a guaranteed copy in hand.
Next steps
- Network filtering, blocked egress shows up as
network.blockedentries. - Credential injection, proxied calls are logged without the secret.
- Endpoint rules, narrow what the credential proxy will forward.