LocalSandbox
A Python SDK for sandboxed filesystem operations, built on just-bash, AgentFS, and Pyodide. Provides AI agents with a persistent, isolated environment backed by SQLite.
⚠️ Warning: This project is in beta. While it provides isolation through WebAssembly and a simulated bash environment, it has not been security audited and should not be relied upon as a fully secure sandbox for running untrusted code. Use at your own risk.
Features
- Sandboxed Execution: Run bash commands in an isolated environment
- Python Execution: Run Python via Pyodide (WebAssembly) on the same virtual filesystem
- Persistent Filesystem: All file operations persist across commands in SQLite
- Key-Value Store: Separate KV API for agent state management
- Command History: Track all executed commands with timestamps and results
- Snapshot & Resume: Export/restore complete sandbox state
- Execution Limits: Configurable DOS protection (loop iterations, command counts)
- Async Support: Full async API via
asyncio.to_thread - Context Manager: Clean resource management with
withstatement
Installation
pip install localsandbox
# or
uv add localsandboxPrerequisites
The package requires Deno to run the TypeScript shim. Install Deno
(brew install deno) and ensure deno is on your PATH.
Quick Start
from localsandbox import LocalSandbox # Basic usage with context manager (recommended) with LocalSandbox() as sandbox: result = sandbox.bash('echo "Hello, World!"') print(result.stdout) # Hello, World! # Without context manager sandbox = LocalSandbox() try: result = sandbox.bash('echo "Hello!"') print(result.stdout) finally: sandbox.destroy() # Seed initial files (all paths use /data prefix) with LocalSandbox(files={"/data/app/main.py": 'print("hello")'}) as sandbox: result = sandbox.execute_python('exec(open("main.py").read())', cwd="/data/app") print(result.stdout) # hello # Use file helpers (all paths use /data prefix) with LocalSandbox() as sandbox: sandbox.write_file("/data/config.json", '{"key": "value"}') content = sandbox.read_file("/data/config.json") exists = sandbox.exists("/data/config.json") files = sandbox.list_files("/data") # Key-value store with LocalSandbox() as sandbox: sandbox.kv.set("user_id", "12345") user_id = sandbox.kv.get("user_id") all_keys = sandbox.kv.keys()
Examples
More runnable scripts are in examples/.
API Reference
LocalSandbox
LocalSandbox( files: dict[str, str | Path | bytes] | None = None, snapshot: bytes | None = None, cwd: str = "/data", preset: ExecutionPreset = ExecutionPreset.NORMAL, )
Parameters:
files: Initial filesystem contents. Supports string content,Pathobjects (read at creation), orbytesfor binary files. All paths should use the/dataprefix.snapshot: Restore from a previously exported snapshot (mutually exclusive withfiles).cwd: Initial working directory (default:/data).preset: Execution limits preset (STRICT,NORMAL, orPERMISSIVE).
Methods
Bash Execution
sandbox.bash(command: str) -> BashResult
Execute a bash command. Returns BashResult with stdout, stderr,
exit_code, and duration_ms.
Raises:
CommandError: Non-zero exit codeFileNotFoundError: File/directory not found (with.pathattribute)PermissionError: Permission denied (with.pathattribute)ExecutionLimitError: Execution limits exceededSubprocessCrashed: Shim subprocess failure
Python Execution
sandbox.execute_python( code: str, cwd: str | None = None, preload_packages: list[str] | None = None, ) -> PythonResult
Execute Python via Pyodide. The sandbox filesystem is mounted at /data in both
bash and Python environments. All paths should use the /data prefix for
consistency across all operations (bash, Python, and file helpers).
If preload_packages is provided, those Pyodide packages are loaded before
execution. No network access is granted unless preloading is requested.
File Operations
sandbox.read_file(path: str) -> str sandbox.write_file(path: str, content: str) -> None sandbox.list_files(path: str) -> list[str] sandbox.exists(path: str) -> bool sandbox.delete_file(path: str) -> None
Key-Value Store
sandbox.kv.get(key: str) -> str | None sandbox.kv.set(key: str, value: str) -> None sandbox.kv.delete(key: str) -> None sandbox.kv.keys(prefix: str = "") -> list[str]
Command History
sandbox.history(limit: int = 100) -> list[HistoryEntry]
Get the history of tool calls executed on this sandbox. Returns a list of
HistoryEntry objects with:
id: Unique identifiername: Tool name (e.g., "bash" or "python")started_at: Unix timestamp when command startedcompleted_at: Unix timestamp when command finishedparameters: Dict withcommand/cwd(bash) orcodeLength/cwd(python)result: Dict withexitCode
from localsandbox import LocalSandbox with LocalSandbox() as sandbox: sandbox.bash('echo "hello"') sandbox.bash('ls -la') history = sandbox.history() for entry in history: print(f"Command: {entry.parameters['command']}, Exit: {entry.result['exitCode']}")
Snapshot & Resume
# Export current state snapshot = sandbox.export_snapshot() # Resume from snapshot new_sandbox = LocalSandbox(snapshot=snapshot)
Lifecycle
sandbox.destroy() # Clean up resources (called automatically by context manager)
Async API
All methods have async versions prefixed with a:
import asyncio from localsandbox import LocalSandbox async def main(): sandbox = LocalSandbox() try: result = await sandbox.abash('echo "async!"') await sandbox.awrite_file("/data/tmp/test.txt", "content") content = await sandbox.aread_file("/data/tmp/test.txt") await sandbox.kv.aset("key", "value") value = await sandbox.kv.aget("key") finally: await sandbox.adestroy() asyncio.run(main())
Execution Presets
| Preset | Max Loop Iterations | Max Commands |
|---|---|---|
| STRICT | 100 | 500 |
| NORMAL | 1,000 | 5,000 |
| PERMISSIVE | 10,000 | 50,000 |
from localsandbox import LocalSandbox, ExecutionPreset # For untrusted input sandbox = LocalSandbox(preset=ExecutionPreset.STRICT) # For complex operations sandbox = LocalSandbox(preset=ExecutionPreset.PERMISSIVE)
Architecture
LocalSandbox uses a TypeScript shim (running on Deno) that bridges Python to:
- just-bash: A bash interpreter/simulator written in TypeScript
- AgentFS: SQLite-based virtual filesystem
- Pyodide: Python interpreter compiled to WebAssembly for sandboxed Python execution
Each operation spawns a Deno subprocess that:
- Opens the SQLite database
- Executes the operation via just-bash or Pyodide
- Persists changes back to SQLite
- Returns JSON results
This architecture provides strong isolation while maintaining state persistence. Both bash and Python share the same virtual filesystem backed by SQLite.
Development
# Install dependencies uv sync # Run tests uv run pytest # Type checking uv run pyright # Lint and format uv run ruff check --fix && uv run ruff format # Shim checks cd shim && deno task check
License
MIT