SandboxExamplesPython examples

Python examples

Every program here is complete: install the SDK, set your key and run it with python. Each one waits for the sandbox to run before using it and deletes it in a finally block. The output under each program is what it printed; ids, versions and the agent’s words differ on your run.

Run the examples

You need Python 3.10 or newer and an API key from id.tangle.tools/app/keys.

pip install tangle-sandbox
export TANGLE_API_KEY="sk-tan-..."
python hello.py

Programs that clone a repository use tangle-network/sandbox-demo, a small Node project with failing tests.

ExampleShows
Hello sandboxCreate a sandbox, run Python in it, write and read a file, ask the coding agent one question, and delete it.
Run one commandRun one command and print its exit code and output.
Analyse a CSVWrite a CSV and a script into the sandbox and run the script.
Stop and resumeStop a sandbox and resume it with its files intact.
Snapshot and restore filesSnapshot a file, break it, restore it from the snapshot, and start a second sandbox from the same snapshot.
Find TODOs in a cloned repositoryClone a repository at create, search it with grep and read its last commit.
Have a coding agent fix a testAsk Claude Code to fix a failing test, then run the test and read the diff yourself.
Fix and commitHave Codex fix a test, check it, and commit only the files it changed.
Watch a coding agent workStream a turn and print each tool the agent calls, then its final answer.
Lock down the networkStart with only one domain allowed, see the refusal, then allow a second domain while the sandbox runs.
Run a batch across two coding agentsRun two tasks on Claude Code and Codex, each in its own sandbox, and print every result.
Run sandboxes concurrently with asyncioCreate three sandboxes at once with AsyncSandbox and gather their results.

Hello sandbox

Create a sandbox, run Python in it, write and read a file, ask the coding agent one question, and delete it.

import os
 
from tangle_sandbox import Sandbox, SandboxStatus
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create(name="hello-python")
    try:
        # create() returns before the sandbox runs; runtime calls need RUNNING.
        box.wait_for(SandboxStatus.RUNNING)
 
        result = box.exec("python3 -c 'print(sum(range(1_000_000)))'")
        print(result.exit_code, result.stdout.strip())
 
        box.write("notes.md", "first draft\n")
        print(box.read("notes.md"), end="")
 
        turn = box.prompt("Reply with exactly the word OK")
        print(turn.response if turn.success else turn.error)
    finally:
        box.delete()
0 499999500000
first draft
OK

Read more: Python SDK.

Run one command

Run one command and print its exit code and output.

import os
 
from tangle_sandbox import Sandbox, SandboxStatus
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create()
    try:
        box.wait_for(SandboxStatus.RUNNING)
        result = box.exec("python3 -c 'import platform; print(platform.platform())'")
        print(result.exit_code, result.stdout.strip())
    finally:
        box.delete()
0 Linux-6.8.0-146-generic-x86_64-with-glibc2.42

The kernel version changes as hosts are updated.

Read more: exec.

Analyse a CSV

Write a CSV and a script into the sandbox and run the script.

import json
import os
 
from tangle_sandbox import Sandbox, SandboxStatus
 
ORDERS = "customer,amount\nacme,1200\nglobex,950\nacme,300\ninitech,80\nglobex,410\n"
TOTALS = """
import csv, json
from collections import defaultdict
totals = defaultdict(float)
for row in csv.DictReader(open("orders.csv")):
    totals[row["customer"]] += float(row["amount"])
print(json.dumps(sorted(totals.items(), key=lambda kv: -kv[1])))
"""
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create()
    try:
        box.wait_for(SandboxStatus.RUNNING)
        box.write("orders.csv", ORDERS)
        box.write("totals.py", TOTALS)
        result = box.exec("python3 totals.py")
        print(result.exit_code, json.loads(result.stdout))
    finally:
        box.delete()
0 [['acme', 1500.0], ['globex', 1360.0], ['initech', 80.0]]

Relative paths resolve in /home/agent, where commands also start.

Read more: Files.

Stop and resume

Stop a sandbox and resume it with its files intact.

import os
 
from tangle_sandbox import Sandbox, SandboxStatus
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create()
    try:
        box.wait_for(SandboxStatus.RUNNING)
        box.write("notes.md", "first draft\n")
 
        box.stop()
        print(box.status.value)
 
        box.resume()
        box.wait_for(SandboxStatus.RUNNING)
        print(box.read("notes.md"), end="")
    finally:
        box.delete()
stopped
first draft

Read more: Lifecycle.

Snapshot and restore files

Snapshot a file, break it, restore it from the snapshot, and start a second sandbox from the same snapshot.

import os
import time
 
from tangle_sandbox import CreateSandboxOptions, Sandbox, SandboxStatus, SnapshotOptions
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create()
    copy = None
    try:
        box.wait_for(SandboxStatus.RUNNING)
        box.write("config.json", '{"retries": 3}\n')
        clean = box.snapshot(SnapshotOptions(tags=["clean"]))
 
        # A new snapshot takes a few seconds to appear in the list.
        for _ in range(60):
            if clean.snapshot_id in [s.snapshot_id for s in box.list_snapshots()]:
                break
            time.sleep(1)
 
        box.write("config.json", '{"retries": "oops"}\n')
        box.restore_paths(clean.snapshot_id, ["config.json"])
        print("restored", box.read("config.json"), end="")
 
        copy = client.create(
            CreateSandboxOptions(from_snapshot=clean.snapshot_id, from_sandbox_id=box.id)
        )
        copy.wait_for(SandboxStatus.RUNNING)
        print("copy", copy.read("config.json"), end="")
    finally:
        box.delete()
        if copy:
            copy.delete()
restored {"retries": 3}
copy {"retries": 3}

Read more: Snapshots.

Find TODOs in a cloned repository

Clone a repository at create, search it with grep and read its last commit.

import os
 
from tangle_sandbox import CreateSandboxOptions, GitConfig, Sandbox, SandboxStatus
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create(
        CreateSandboxOptions(git=GitConfig(url="https://github.com/tangle-network/sandbox-demo"))
    )
    try:
        box.wait_for(SandboxStatus.RUNNING)
        # Commands start in the cloned repository.
        hits = box.exec("grep -rn --include='*.js' TODO packages src")
        print(hits.stdout, end="")
        print(box.git.log(limit=1)[0].message)
    finally:
        box.delete()
packages/api/src/server.js:36:    // TODO: check that the user exists
packages/cli/src/flags.js:12:      flags[name] = next; // TODO: numbers stay strings
packages/web/src/client.js:13:// TODO: expire cached entries
packages/worker/src/queue.js:12:    if (job) job.run(); // TODO: retry failed jobs
Add a lockfile so the share-a-preview quickstart's npm ci installs (#2)

Read more: Git.

Have a coding agent fix a test

Ask Claude Code to fix a failing test, then run the test and read the diff yourself.

import os
 
from tangle_sandbox import (
    BackendConfig,
    CreateSandboxOptions,
    ExecOptions,
    GitConfig,
    PromptOptions,
    Sandbox,
    SandboxStatus,
)
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create(
        CreateSandboxOptions(
            git=GitConfig(url="https://github.com/tangle-network/sandbox-demo"),
            backend=BackendConfig(type="claude-code"),
        )
    )
    try:
        box.wait_for(SandboxStatus.RUNNING)
        run = box.prompt(
            "The test tagged null-guard fails. Find the cause and fix it.",
            PromptOptions(timeout_ms=600_000),
        )
        if not run.success:
            raise RuntimeError(run.error)
 
        # Check the result yourself.
        tests = box.exec(
            "npm test -- --test-name-pattern=null-guard",
            ExecOptions(timeout_ms=120_000),
        )
        print("tests exit", tests.exit_code)
 
        diff = box.git.diff()
        print(f"+{diff.additions} -{diff.deletions}", [f.get("path") for f in diff.files])
        print(run.usage)
    finally:
        box.delete()
tests exit 0
+2 -1 ['packages/shared/src/format.js']
TokenUsage(input_tokens=119425, output_tokens=1265)

The agent’s turn can take a few minutes; timeout_ms is the longest wait between its events.

Read more: prompt, Run a coding agent.

Fix and commit

Have Codex fix a test, check it, and commit only the files it changed.

import os
 
from tangle_sandbox import (
    BackendConfig,
    CreateSandboxOptions,
    GitConfig,
    PromptOptions,
    Sandbox,
    SandboxStatus,
)
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create(
        CreateSandboxOptions(
            git=GitConfig(url="https://github.com/tangle-network/sandbox-demo"),
            backend=BackendConfig(type="codex"),
        )
    )
    try:
        box.wait_for(SandboxStatus.RUNNING)
        run = box.prompt(
            "The test tagged null-guard fails. Fix the cause.",
            PromptOptions(timeout_ms=600_000),
        )
        if not run.success:
            raise RuntimeError(run.error)
 
        changed = [f["path"] for f in box.git.diff("HEAD").files]
        print("changed", changed)
        tests = box.exec("npm test -- --test-name-pattern=null-guard")
        if tests.exit_code != 0:
            raise RuntimeError("The test still fails.")
 
        # The image has no git identity; git.commit sends no author.
        box.exec('git config --global user.name "Docs Example"')
        box.exec("git config --global user.email [email protected]")
        box.git.add(changed)
        commit = box.git.commit("fix: guard a null request body")
        print(commit.short_sha, commit.message)
    finally:
        box.delete()
changed ['packages/shared/src/format.js']
8d1ace2 fix: guard a null request body

Read more: Git.

Watch a coding agent work

Stream a turn and print each tool the agent calls, then its final answer.

import os
 
from tangle_sandbox import (
    BackendConfig,
    CreateSandboxOptions,
    GitConfig,
    PromptOptions,
    Sandbox,
    SandboxStatus,
)
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create(
        CreateSandboxOptions(
            git=GitConfig(url="https://github.com/tangle-network/sandbox-demo"),
            backend=BackendConfig(type="claude-code"),
        )
    )
    try:
        box.wait_for(SandboxStatus.RUNNING)
        seen = set()
        events = box.stream_prompt(
            "The test tagged timezone fails. Fix the cause.",
            PromptOptions(timeout_ms=600_000),
        )
        for event in events:
            part = event.data.get("part") or {}
            if event.type == "message.part.updated" and part.get("type") == "tool":
                if part.get("id") not in seen:
                    seen.add(part.get("id"))
                    print(f"{len(seen)}.", part.get("tool"))
            if event.type == "result":
                print(event.data.get("finalText"))
    finally:
        box.delete()
1. Bash
2. Bash
3. Bash
4. Read
5. Read
6. Bash
7. Edit
8. Bash
9. Bash
10. Bash
11. Bash
Fixed.

**Root cause:** `dayKey()` in `packages/shared/src/dates.js` used local-time getters ...
**Fix:** switched to the UTC equivalents ...

Read more: stream_prompt, Stream output.

Lock down the network

Start with only one domain allowed, see the refusal, then allow a second domain while the sandbox runs.

import os
 
from tangle_sandbox import (
    CreateSandboxOptions,
    EgressPolicy,
    Sandbox,
    SandboxStatus,
)
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    box = client.create(
        CreateSandboxOptions(
            egress_policy=EgressPolicy(mode="strict", allow_domains=["pypi.org"]),
        )
    )
    try:
        box.wait_for(SandboxStatus.RUNNING)
        blocked = box.exec("curl -sS -m 5 -o /dev/null https://example.com")
        print("example.com exit", blocked.exit_code)
 
        for denial in box.egress_denials():
            print("refused", denial.host, denial.count)
 
        policy, source = box.egress_update(
            EgressPolicy(mode="strict", allow_domains=["pypi.org", "example.com"])
        )
        print(policy.mode, policy.allow_domains, source)
        allowed = box.exec("curl -sS -m 5 -o /dev/null -w '%{http_code}' https://example.com")
        print("example.com", allowed.stdout)
    finally:
        box.delete()
example.com exit 7
refused example.com 1
strict ['pypi.org', 'example.com'] sandbox
example.com 200

curl exit code 7 means the connection was refused.

Read more: Network rules, Network access.

Run a batch across two coding agents

Run two tasks on Claude Code and Codex, each in its own sandbox, and print every result.

import os
 
from tangle_sandbox import BatchBackend, BatchRunRequest, BatchTask, Sandbox
 
request = BatchRunRequest(
    tasks=[
        BatchTask(id="capital", message="Reply with only the capital of France."),
        BatchTask(id="sum", message="Reply with only the result of 17 + 25."),
    ],
    # Each task runs once on each backend, each in its own sandbox.
    backends=[
        BatchBackend(id="claude", type="claude-code"),
        BatchBackend(id="codex", type="codex"),
    ],
)
 
with Sandbox(
    api_key=os.environ["TANGLE_API_KEY"],
    base_url="https://sandbox.tangle.tools",
) as client:
    result = client.run_batch(request)
    for task in result.results:
        print(task.task_id, task.backend_id, task.success, task.result_summary or task.error)
    print(f"{result.total_success}/{result.total_executions} succeeded")
capital claude True Paris
capital codex True Paris
sum claude True 42
sum codex True 42
4/4 succeeded

Read more: run_batch, Batches and hosted runs.

Run sandboxes concurrently with asyncio

Create three sandboxes at once with AsyncSandbox and gather their results.

import asyncio
import os
 
from tangle_sandbox import AsyncSandbox, SandboxStatus
 
 
async def word_count(client: AsyncSandbox, text: str) -> str:
    box = await client.create()
    try:
        await box.wait_for(SandboxStatus.RUNNING)
        await box.write("input.txt", text)
        result = await box.exec("wc -w < input.txt")
        return result.stdout.strip()
    finally:
        await box.delete()
 
 
async def main() -> None:
    async with AsyncSandbox(
        api_key=os.environ["TANGLE_API_KEY"],
        base_url="https://sandbox.tangle.tools",
    ) as client:
        texts = ["one two three", "four five", "six"]
        counts = await asyncio.gather(*(word_count(client, t) for t in texts))
        print(counts)
 
 
asyncio.run(main())
['3', '2', '1']

Read more: Async client.

Tasks with no Python method

These examples use calls the Python SDK does not have. Run them with the TypeScript SDK, or call the REST route with client.fetch.

TaskExample
Serve a port on a public linkHost a page on a public URL
Run a dev server in the backgroundShare a dev server
Revert a whole sandbox to a snapshotSnapshot and revert
Copy a running sandboxBranch into two
Continue a named agent session after resumePause a coding agent and continue later
FleetsFleets

Next