Run Claude Code with --dangerously-skip-permissions safely inside a Daytona sandbox.
pip install .Requires Python 3.10 or later.
| Variable | Required | Description |
|---|---|---|
DAYTONA_API_KEY |
Yes | Your Daytona API key (read by the Daytona SDK) |
ANTHROPIC_API_KEY |
Yes | Your Anthropic API key (or pass it directly via anthropic_api_key=) |
These variables are read by their respective SDKs, not by Maison directly. The Daytona SDK also supports optional DAYTONA_API_URL and DAYTONA_TARGET variables for custom deployments.
After installing, the maison-cli command is available. It spins up a Daytona sandbox, installs Claude Code, and connects you to it.
Run maison-cli with no arguments to start a multi-turn chat session. Claude retains context across messages, and the sandbox is automatically deleted when you type quit or press Ctrl+C.
maison-cliPass -p to run a single prompt and exit:
maison-cli -p "Write a hello world program in Python"| Flag | Description |
|---|---|
-p, --prompt |
Run a single prompt and exit |
--instructions |
Custom instructions appended to Claude's system prompt |
--snapshot |
Daytona sandbox image (default: daytona-small) |
--debug |
Print raw event data for debugging |
Spin up a sandbox, give Claude a task, then iterate on it with multi-turn follow-ups — all inside an isolated environment where Claude has full permissions:
import asyncio
from maison import Maison
async def main():
# 1. Create an isolated sandbox with Claude Code installed
sandbox = await Maison.create_sandbox_for_claude()
try:
# 2. First turn — give Claude a task
async for event in sandbox.stream(
"Create a Python FastAPI app with a /health endpoint and a /items CRUD endpoint. "
"Include a requirements.txt.",
instructions="Use type hints everywhere. Keep it production-ready.",
):
if event.type == "text":
print(event.content, end="", flush=True)
print()
# 3. Follow-up turns — Claude remembers everything from above
async for event in sandbox.stream(
"Add pytest tests for both endpoints and make sure they pass.",
continue_conversation=True,
):
if event.type == "text":
print(event.content, end="", flush=True)
print()
# 4. Pull files out of the sandbox
app_code = await sandbox.read_file("/home/daytona/main.py")
print(app_code)
finally:
# 5. Clean up — deletes the sandbox
await sandbox.close()
asyncio.run(main())Each stream() call yields StreamEvent objects in real time. Set continue_conversation=True on follow-up turns so Claude retains the full context from earlier messages.
Creates a Daytona sandbox and installs Claude Code.
| Parameter | Default | Description |
|---|---|---|
anthropic_api_key |
$ANTHROPIC_API_KEY |
Anthropic API key |
snapshot |
"daytona-small" |
Daytona snapshot image |
name |
None |
Optional sandbox name |
Raises: ValueError if no Anthropic API key is provided or found in $ANTHROPIC_API_KEY. RuntimeError if Node.js or Claude Code installation fails in the sandbox.
Runs Claude Code with the given prompt and yields StreamEvent objects as they arrive. Includes thinking tokens, text deltas, tool use, and the final result.
| Parameter | Default | Description |
|---|---|---|
prompt |
(required) | The task or question for Claude Code |
instructions |
None |
Custom instructions appended to Claude Code's system prompt |
continue_conversation |
False |
Continue the most recent conversation so Claude retains prior context |
poll_interval |
0.3 |
Seconds between file polls for new output |
Raises: RuntimeError if the claude binary is not found in the sandbox (checked on first call).
Reads a file from the sandbox filesystem.
Deletes the sandbox and frees resources.
| Field | Type | Description |
|---|---|---|
type |
str |
Event type: "thinking", "text", "tool_use", "result", or "stderr" |
data |
dict |
Raw JSON event from Claude Code |
content |
str |
Convenience property that extracts text content |
A "stderr" event is emitted at the end of a stream() call if Claude Code wrote anything to stderr.
Use continue_conversation=True to send follow-up messages that retain full context from earlier turns:
sandbox = await Maison.create_sandbox_for_claude()
# First message — starts a new conversation
async for event in sandbox.stream("Create a Python Flask app with a /health endpoint"):
if event.type == "text":
print(event.content, end="")
# Second message — continues the same conversation
async for event in sandbox.stream(
"Now add a /users endpoint with GET and POST",
continue_conversation=True,
):
if event.type == "text":
print(event.content, end="")See examples/multi_turn.py for a complete interactive chat loop.
create_sandbox_for_claude()spins up an isolated Daytona sandbox, installs Node.js (if needed), and installs Claude Code globally via npm.stream()creates a persistent Daytona session (reused across calls for multi-turn) and runsclaude --dangerously-skip-permissions -p <prompt> --output-format stream-json --verbosewith output redirected to temporary files.- Maison polls the output file for new NDJSON lines at a configurable interval (default 0.3 s), parsing each line into a
StreamEvent. A completion marker file signals the end of the stream. - Because Claude runs inside the sandbox, it has full permissions without risking your host machine.
MIT