Run arbitrary code in a sandboxed environment

This example demonstrates how to run arbitrary code in multiple languages inside a Lightning Sandbox using the lightning-sdk.

A sandbox is a fully isolated environment, so you can run any code in it -- even genuinely dangerous code -- without affecting your own machine.

Copy-paste friendly: every Python block below is part of one script. If you paste them in order into a file (e.g. arbitrary_code_sandbox.py) and drop in your API key, you get a complete, runnable program -- the same one shipped in the SDK repo. The last section shows how to grab and run it straight from GitHub.

Authentication

The SDK reads your credentials from the environment once at import time. Rather than rely on import order, configure your key in-process with Sandbox.configure(...):

from lightning_sdk.sandbox import RunCommandOpts, Sandbox

Sandbox.configure(api_key="sk-lit-...")

You can also set LIGHTNING_SANDBOX_API_KEY in your environment instead. See Authentication for how to create a teamspace-scoped key.

A helper for long-running commands

sandbox.run_command(...) runs synchronously and returns the full output once the process finishes -- which is all most steps here need. Synchronous calls have a ~120s server-side deadline, though, so for long-running work (installing a toolchain) we launch the command detached and watch a file it writes.

The robust pattern: run the script in a subshell that redirects its combined output to a file, and drop an exit-code "sentinel" file the instant it finishes. Then poll read_file (which is reliable) until that sentinel appears.

import time
import uuid

MINUTES_MS = 60 * 1000


def run_and_wait(
    sandbox: Sandbox, script: str, *, timeout: float, poll_interval: float = 2.0
) -> tuple[int, str]:
    """Run a bash ``script`` in the sandbox and block until it exits.

    Returns ``(exit_code, combined_output)``.
    """
    token = uuid.uuid4().hex
    out_path = f"/tmp/.cmd_{token}.out"
    done_path = f"/tmp/.cmd_{token}.done"
    # Run the script in a subshell so the redirect captures all of it, then
    # record the exit code in the sentinel file the instant the subshell exits.
    wrapped = f"(\n{script}\n) >{out_path} 2>&1\necho $? >{done_path}\n"
    sandbox.run_command(
        RunCommandOpts(cmd="bash", args=["-c", wrapped], detached=True)
    )

    deadline = time.time() + timeout
    while time.time() < deadline:
        exit_code = sandbox.read_file(done_path)
        if exit_code and exit_code.strip():
            return int(exit_code.strip()), (sandbox.read_file(out_path) or "")
        time.sleep(poll_interval)
    raise TimeoutError(f"command did not finish within {timeout}s")

Setting up a multi-language environment

To show off what a sandbox can do, we want a box that can run bash, Python, Node.js, Ruby, and PHP.

Lightning AI's curated runtimes ship one language each -- a python313 box has no node, a node24 box has no python. So we start from python313 (which gives us Python and bash) and apt-get install the remaining runtimes into that same sandbox. Installing packages needs egress, which is on by default.

The install takes longer than the synchronous deadline, so we run it with the run_and_wait helper from above:

def setup_multi_language_sandbox() -> Sandbox:
    """Create a sandbox and install Node.js, Ruby and PHP alongside Python."""
    print("๐Ÿ–๏ธ  Creating sandbox (python313 base)...")

    sandbox = Sandbox.create(
        name="example-safe-code-execution",
        instance_type="cpu-1",
        runtime="python313",
        timeout=30 * MINUTES_MS,
    )

    print(f"Sandbox ID: {sandbox.sandbox_id}")

    print("๐Ÿ–๏ธ  Installing nodejs, ruby and php (this can take a minute)...")
    install_script = (
        "set -eux; "
        "export DEBIAN_FRONTEND=noninteractive; "
        "apt-get update; "
        "apt-get install -y nodejs ruby php-cli"
    )
    exit_code, output = run_and_wait(sandbox, install_script, timeout=600)
    if exit_code != 0:
        raise RuntimeError(
            f"toolchain install failed (exit {exit_code}):\n{output}"
        )

    return sandbox

Tip: Once created, the sandbox stays alive (until its timeout) waiting for commands.

Running bash, Python, Node.js, Ruby, and PHP

We can now run commands in the sandbox with run_command(...). Each snippet here is short, so we run them synchronously -- one call each returns the full output once the process exits:

def run_in_each_language(sandbox: Sandbox) -> None:
    """Run bash, Python, Node.js, Ruby and PHP in the sandbox."""
    print("\n--- Running bash, Python, Node.js, Ruby and PHP ---")

    commands = [
        ("bash", ["-c", "echo 'hello from bash'"]),
        ("python", ["-c", "print('hello from python')"]),
        ("node", ["-e", 'console.log("hello from nodejs")']),
        ("ruby", ["-e", "puts 'hello from ruby'"]),
        ("php", ["-r", "echo 'hello from php';"]),
    ]

    for cmd, cmd_args in commands:
        result = sandbox.run_command(cmd, cmd_args)
        print(result.output.rstrip())

The output should look something like

hello from bash
hello from python
hello from nodejs
hello from ruby
hello from php

We can use multiple languages in tandem to build complex applications. Let's demonstrate this by piping data between Python and Node.js using bash. Here we generate some random numbers with Python and sum them with Node.js. This one is quick, so it runs synchronously and we read its output directly:

def pipe_python_into_node(sandbox: Sandbox) -> None:
    """Pipe data between Python and Node.js using bash."""
    print("\n--- Piping Python -> Node.js via bash ---")

    combined = sandbox.run_command(
        "bash",
        [
            "-c",
            """python -c 'import random; print(" ".join(str(random.randint(1, 100)) for _ in range(10)))' |
            node -e 'const readline = require("readline");
            const rl = readline.createInterface({input: process.stdin});
            rl.on("line", (line) => {
              const sum = line.split(" ").map(Number).reduce((a, b) => a + b, 0);
              console.log(`The sum of the random numbers is: ${sum}`);
              rl.close();
            });'""",
        ],
    )
    print(combined.output.strip())

Streaming a long-running process

For long-running processes, you can stream the output as it's produced. There's no stdout iterator for a running command, so we launch the process detached with its output redirected to a file, then poll that file with read_file and print only the newly-appended bytes each tick. Ruby flushes stdout so the lines land in the file incrementally.

def stream_long_running_process(sandbox: Sandbox) -> None:
    """Stream output from a long-running process as it is produced."""
    print("\n--- Streaming a long-running Ruby process ---")

    ruby_script = """
    10.times do |i|
      puts "Line #{i + 1}: #{Time.now}"
      STDOUT.flush
      sleep(0.5)
    end
    """
    # Write the script to a file to avoid nested shell quoting, then launch it
    # detached with output going to a file we can tail.
    sandbox.write_file("/root/slow_printer.rb", ruby_script)
    token = uuid.uuid4().hex
    out_path = f"/tmp/.stream_{token}.out"
    done_path = f"/tmp/.stream_{token}.done"
    wrapped = f"ruby /root/slow_printer.rb >{out_path} 2>&1\necho $? >{done_path}\n"
    sandbox.run_command(
        RunCommandOpts(cmd="bash", args=["-c", wrapped], detached=True)
    )

    printed = 0
    while True:
        output = sandbox.read_file(out_path) or ""
        if len(output) > printed:
            print(output[printed:], end="", flush=True)
            printed = len(output)
        if (sandbox.read_file(done_path) or "").strip():
            break
        time.sleep(0.25)

This should print something like

Line 1: 2026-06-15 15:30:53 +0000
Line 2: 2026-06-15 15:30:54 +0000
...
Line 10: 2026-06-15 15:30:58 +0000

Running dangerous code

Since sandboxes are safely separated from the rest of our system, we can run very dangerous code in them!

def run_dangerous_code(sandbox: Sandbox) -> None:
    """Run genuinely destructive code -- safe because the sandbox is isolated."""
    print("\n--- Running dangerous code (rm -rf / inside the sandbox) ---")
    try:
        sandbox.run_command("rm", ["-rfv", "/", "--no-preserve-root"])
    except RuntimeError as err:
        # Wiping the filesystem can knock out the in-sandbox agent mid-command;
        # that's expected here since we're about to delete the sandbox anyway.
        print(f"(sandbox stopped responding, as expected: {err})")
    print("The sandbox filesystem has been wiped.")

This command has deleted the entire filesystem, so we can't run any more commands.

Putting it together

Finally, main() wires everything up and tears the sandbox down when we're done -- remote sandboxes are not garbage-collected, so deleting it is important (in production you'd wrap the run in try/finally so a mid-run error still cleans up):

def main() -> None:
    Sandbox.configure(api_key="sk-lit-...")

    sandbox = setup_multi_language_sandbox()
    run_in_each_language(sandbox)
    pipe_python_into_node(sandbox)
    stream_long_running_process(sandbox)
    run_dangerous_code(sandbox)

    print("\n๐Ÿ–๏ธ  Terminating sandbox...")
    sandbox.delete()
    print("Done.")


if __name__ == "__main__":
    main()

Run it with your API key in the environment:

export LIGHTNING_SANDBOX_API_KEY=sk-lit-...
python arbitrary_code_sandbox.py

The output should look something like

๐Ÿ–๏ธ  Creating sandbox (python313 base)...
Sandbox ID: <sandbox-id>
๐Ÿ–๏ธ  Installing nodejs, ruby and php (this can take a minute)...

--- Running bash, Python, Node.js, Ruby and PHP ---
hello from bash
hello from python
hello from nodejs
hello from ruby
hello from php

--- Piping Python -> Node.js via bash ---
The sum of the random numbers is: 491

--- Streaming a long-running Ruby process ---
Line 1: 2026-06-15 15:30:53 +0000
Line 2: 2026-06-15 15:30:54 +0000
... (streamed live, ~0.5s apart) ...
Line 10: 2026-06-15 15:30:58 +0000

--- Running dangerous code (rm -rf / inside the sandbox) ---
The sandbox filesystem has been wiped.

๐Ÿ–๏ธ  Terminating sandbox...
Done.

Run the complete example

You don't have to assemble the snippets by hand -- the complete, ready-to-run script (with --api-key, --instance-type, and other CLI flags) lives in the SDK repo. Clone it and run it directly:

git clone https://github.com/Lightning-AI/sdk.git
cd sdk/python/examples

pip install lightning-sdk
export LIGHTNING_SANDBOX_API_KEY=sk-lit-...   # or pass --api-key sk-lit-...

python arbitrary_code_sandbox.py

Pass --help to see all available options:

python arbitrary_code_sandbox.py --help

Was this page helpful?

What went wrong?

โ†‘โ†“ Navigateโ†ต OpenโŒ˜โ†ต New tabesc Close