Skip to main content

sanduk

Run an agent inside a disposable container. The agent does its work, writes a report to a bind-mounted directory, and the container is deleted.

Today that means Claude Code on macOS through Apple's container. The container engine sits behind sanduk.runtime.Runtime and the agent CLI behind sanduk.agent, so Docker, Podman, and other agents are additive.

In its stronger mode the VM has no route off the host and never holds the API key: a host-side relay injects the credential, and the container gets a per-run token that is worthless anywhere else.

'sanduk' means 'box' in Arabic.

Requirements

  • Mac with Apple silicon, macOS 26 or later (container requires both)

  • container 1.2.0 or later

  • Python 3.11 or later, and uv

  • An Anthropic API key in ANTHROPIC_API_KEY

Apple's container runs Linux containers as lightweight VMs. There is no such thing as a macOS container here; anything needing Xcode or the macOS toolchain cannot be the workload.

The image is node:22-slim plus git, ripgrep, curl, jq, and python3. The agent can only run what is in it. Without an interpreter it falls back to hand-tracing and still writes a confident report, so check whether the findings say they were reproduced. There is no C, Go, or Rust toolchain: point --image at your own, or --containerfile at one to build.

Quickstart

make sync
make image
export ANTHROPIC_API_KEY=sk-ant-...
make run TASK='Summarise every Python file here.' WORK=./work

Or directly, which is where all the flags live:

uv run sanduk 'Summarise every Python file here.' -w ./work --proxy
uv run sanduk --help

Two modes

default --proxy
Host filesystem VM isolation VM isolation
API key location inside the VM host only; VM holds a run token
Egress unrestricted none
Agent can POST your source anywhere yes no
Which endpoints the agent may call all three, matched exactly
Record of what it sent upstream none --log-bodies

The default mode is filesystem isolation and nothing more. --proxy is where the containment is.

How --proxy works

  1. sanduk-net is created with --internal: no route off the host.

  2. vmnet only creates the host bridge while a container is attached, so a placeholder container is started first and torn down at the end.

  3. The relay binds the bridge gateway only, so it is unreachable from Wi-Fi or LAN.

  4. The container is given ANTHROPIC_BASE_URL and a per-run token as its ANTHROPIC_API_KEY. Both arrive through the child process environment, so neither appears in ps, and container inspect shows the token, not the key.

  5. The relay checks the token, checks the path against an exact allowlist, applies any model or token policy, swaps in the real key, and streams the response back.

  6. Every relayed call logs its token counts: in=, cache_write=, cache_read=, out=. A /v1/messages call that reports none logs usage=? rather than a line that looks ordinary. The relay narrows the client's Accept-Encoding to gzip to read them, because the API answers in brotli whenever a client offers it and nothing in the standard library decodes brotli.

Flags worth knowing

--allow-model claude-opus-5 and --max-tokens-cap N are enforced on the host, where the container cannot edit them. Claude Code asks for max_tokens: 64000 on every call, so a cap below that silently truncates every request.

--log-bodies records each request body: a digest line per call, full JSON under --log-dir (default ./sanduk-logs, deliberately outside the bind mount so the agent cannot read or edit its own audit trail). Bodies contain the system prompt and every file the agent has read.

--dry-run prints the container run command and exits. --keep leaves the container for inspection, and warns that container inspect then exposes the token.

Layout

src/sanduk/
    cli.py         flags, lifecycle, teardown
    runtime.py     container engines; ContainerSpec; only `apple` is implemented
    agent.py       the agent CLI inside the container; only Claude Code
    proxy.py       the host-side relay
    preflight.py   key validation, macOS firewall check
    resources/
        Containerfile

A second engine is a Runtime subclass and a RUNTIMES entry. It must supply four things: the CLI name, the verb that deletes a container (rm, not delete), how network inspect reports the gateway, and whether the host bridge needs a placeholder container to exist at all.

Make targets

make help lists all of them. The ones you need:

make sync             Resolve and install the environment
make test             Fast suite: no containers, no API calls, no key needed
make test-container   Integration suite: boots real containers
make test-all         Both
make qa               lint-check, format-check, typecheck, test
make image            Build the agent image if missing
make image-rebuild    Force a rebuild
make run              TASK='...' WORK=./dir ARGS='--effort max'
make run-proxy        Same, with no egress and the key held on the host
make shell            Interactive shell in the image
make ps / make logs   Containers / recorded request bodies
make stop             Stop sanduk containers, leave them on disk
make clean            Delete them and build scratch. Keeps work/ and logs
make distclean        clean, plus .venv and tool caches
make destroy          clean, plus the image, the network, and the logs
make system-start / system-stop / system-status

make system-stop stops Apple's container service for everything on the machine, not just sanduk.

Testing

The fast suite makes no API calls and needs no key: the relay is exercised against a local fake upstream, and the preflight is monkeypatched. scripts/sanduk.py carries its own copy of the relay, so a test compares every function the two share and fails on any difference outside a listed set of architectural ones. The integration suite boots real VMs and proves the relay by the 401 an invalid key earns from the real endpoint, which is itself proof the request arrived.

Measured on this setup

Bad key, host preflight 0.27s
Bad key, no preflight 174s of in-container retry backoff
Agent run, 3 files, 7 turns 35.4s wall, 23.2s of it upstream
Review of one 59-line file, 10 turns $0.55, 148s wall
Claude Code's system prompt and tool schemas 22,993 tokens
That prefix written cold, as a share of one run 25% of its cost
The same prefix on a second run inside the cache TTL read, not written: 23% cheaper
Container direct egress on sanduk-net 000
Real key present in container environment 0 occurrences

Known traps

The macOS application firewall silently drops connections to a binary set to "Block incoming connections", so the agent's first API call hangs until --timeout rather than failing. Homebrew's Python is shipped blocked on at least one machine; uv's interpreters are signed and auto-allowed. --proxy runs a preflight that names the exact socketfilterfw --unblockapp command when it sees an explicit block. It cannot detect an interpreter that will merely prompt.

AF_UNIX paths cap at 104 bytes on macOS, which matters if you point --log-dir somewhere deep.

Metadata

Release files for sanduk 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sanduk 0.1.0
File Size Uploaded Provenance
sanduk-0.1.0.tar.gz 31.9 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sanduk 0.1.0
File Interpreter ABI Platform
sanduk-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 54.4 kB

Release files / sanduk-0.1.0.tar.gz

Download URL sanduk-0.1.0.tar.gz
Size 31.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4bd23d815c402d1739e4d91383411a7a07be1f354f566a4a172f33a6f1fa4ad0
BLAKE2b-256 checksum
How to use checksums
7d2ca973662f798bbca50eafaa93a00129cc4635463e7e59074d0aa27d29a2b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Release files / sanduk-0.1.0-py3-none-any.whl

Download URL sanduk-0.1.0-py3-none-any.whl
Size 22.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3543de4456ab882ae252916935ee4746b8ad65bc1105a3291015c155f4256398
BLAKE2b-256 checksum
How to use checksums
d001bc80571939fcf21bfd9041e5e8187790bb69cb55c08a6cc17345d580fe4f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.7

Security for sanduk 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page