Skip to content

Remote Claude Code Sessions

Run Claude Code as a remote headless agent inside an isolated Baponi sandbox. Credentials for Anthropic and GitHub are injected automatically through connectors. Your code never handles API keys or tokens directly.

  1. Baponi account and API key

    Sign up at console.baponi.ai and create an API key. 1,000 free credits/month, no credit card.

  2. Anthropic connector

    In the Baponi admin UI, go to Connectors → Add → Anthropic. Paste your Anthropic API key. Attach the connector to the API key you’ll use for executions.

    This injects ANTHROPIC_API_KEY as an environment variable into every sandbox execution, and Claude Code picks it up automatically.

  3. GitHub connector (optional, for private repos and pushing)

    Go to Connectors → Add → GitHub. Paste a fine-grained personal access token with Contents: Read and write permission on the repos you want the agent to access. Attach it to the same API key.

    This injects a .gitconfig that transparently rewrites https://github.com/ URLs to include your token, plus a GH_TOKEN environment variable for the gh CLI. git clone, git push, and gh commands work on private repos without any token handling in code.

    See Connectors for the full list of supported integrations (databases, cloud storage, developer tools).

  4. Install the Python SDK (optional, for the Python examples)

    Terminal window
    pip install baponi

Run a single Claude Code task inside a sandbox:

Terminal window
curl -X POST https://api.baponi.ai/v1/sandbox/execute \
-H "Authorization: Bearer $BAPONI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"code": "claude -p \"Research the current price of Bitcoin and summarize your findings\" --dangerously-skip-permissions",
"language": "bash",
"timeout": 300
}'

Claude Code runs inside the sandbox with full system access: it can make HTTP requests, install packages, read and write files, and run code.

With both the Anthropic and GitHub connectors attached, Claude Code can autonomously discover repositories, make changes, and push, all within a single sandbox execution.

from baponi import Baponi
client = Baponi()
result = client.execute(
code='''claude -p "List the GitHub repos available to you. Pick the least important one you can write to. Add a '## Fun Fact' section to its README with a funny sandbox coding joke. Commit and push." \
--dangerously-skip-permissions \
--max-turns 20''',
language="bash",
timeout=600,
thread_id="github-agent-session",
)
print(result.stdout)

Claude Code handles the entire workflow: it calls gh repo list to discover available repositories, evaluates which one to target, clones it, reads the README, makes the edit, commits, and pushes, all with credentials injected transparently through connectors.

For more control, split the workflow into persistent steps using thread_id. Each call builds on the previous one. Cloned repos, installed packages, and generated files all persist.

from baponi import Baponi
client = Baponi()
thread = "feature-auth-refactor"
# Step 1: Clone the repo (uses GitHub connector credentials automatically)
clone = client.execute(
code="git clone https://github.com/your-org/your-repo.git /home/baponi/repo",
language="bash",
timeout=60,
thread_id=thread,
)
assert clone.exit_code == 0, f"Clone failed: {clone.stderr}"
# Step 2: Run Claude Code on the codebase
agent = client.execute(
code='''cd /home/baponi/repo && claude -p "Refactor the auth module to use async/await. Update all callers. Run the test suite and fix any failures." \
--dangerously-skip-permissions \
--output-format stream-json \
--max-turns 50''',
language="bash",
timeout=3600, # 1 hour for complex tasks
thread_id=thread,
)
print(agent.stdout)
# Step 3: Push the changes
push = client.execute(
code='''cd /home/baponi/repo \
&& git add -A \
&& git commit -m "refactor: migrate auth module to async/await" \
&& git push origin HEAD''',
language="bash",
timeout=60,
thread_id=thread,
)
assert push.exit_code == 0, f"Push failed: {push.stderr}"

Each step uses the same thread_id, so the cloned repo from step 1 is still on disk when step 2 runs Claude Code, and the changes from step 2 are still there when step 3 pushes.

Key flags for headless sandbox execution:

FlagPurpose
-p "prompt"Non-interactive mode. Runs the prompt and exits. Required in sandboxes.
--dangerously-skip-permissionsAuto-approve all tool calls. Required in sandboxes (no TTY for prompts).
--output-format stream-jsonStructured JSON output for parsing agent actions programmatically.
--max-turns 50Safety rail: stop after N tool-use turns to prevent runaway loops.
--max-budget-usd 10.00Cost ceiling for the Anthropic API calls within the session.
--allowedTools "Bash,Read,Edit,Write"Pre-approve specific tools (alternative to --dangerously-skip-permissions).

The claude-agent-sdk gives you programmatic control over the agent loop. Your backend acts as the control plane, approving or denying every tool call while the agent runs isolated in a Baponi sandbox.

The example below runs a minimal Flask control plane inside the sandbox to demonstrate the pattern. In production, your control plane is a server you run anywhere (your own infrastructure, a cloud function, etc.) and the SDK hooks call out to it over the network.

Since flask is not in the default image, install it first. Using the same thread_id across calls means installed packages and files persist between steps.

The control plane server below is a minimal example that approves every tool call. In production, this would be your own server running anywhere outside the sandbox, enforcing whatever approval policy you need:

control_plane.py
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.post("/approve-tool")
def approve():
tool = request.json["tool"]
print(f"[control-plane] Approved: {tool}")
return jsonify(approved=True) # Your policy logic goes here
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5100)

The agent script starts the control plane in the background, then runs the SDK with a PreToolUse hook that routes every tool call through it for approval:

agent.py
import asyncio, subprocess, time
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
from claude_agent_sdk.types import (
AssistantMessage, ResultMessage, TextBlock,
HookInput, HookMatcher,
)
import httpx
# Start the control plane in the background
server = subprocess.Popen(["python3", "/home/baponi/control_plane.py"])
time.sleep(2)
async def approve_hook(input_data, tool_use_id, context):
async with httpx.AsyncClient() as http:
resp = await http.post("http://127.0.0.1:5100/approve-tool", json={
"tool": input_data["tool_name"],
"input": input_data["tool_input"],
})
if not resp.json().get("approved"):
return {"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
}}
return {}
async def main():
options = ClaudeAgentOptions(
cli_path="/home/baponi/.local/bin/claude",
allowed_tools=["Bash", "Read", "Write", "Edit"],
hooks={"PreToolUse": [HookMatcher(matcher=".*", hooks=[approve_hook])]},
)
async with ClaudeSDKClient(options=options) as sdk:
await sdk.query("Write a haiku to /home/baponi/haiku.txt, then read it back")
async for msg in sdk.receive_response():
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, TextBlock):
print(block.text)
elif isinstance(msg, ResultMessage):
break
asyncio.run(main())
server.terminate()

Tie it together with the Baponi SDK. Write both files to the sandbox, then run the agent:

from baponi import Baponi
client = Baponi()
thread = "sdk-controlled-session"
# Install Flask (persists via thread_id)
client.execute(code="pip install flask", language="bash", timeout=60, thread_id=thread)
# Write both scripts to the persistent filesystem
CONTROL_PLANE_PY = "..." # Contents of control_plane.py above
AGENT_PY = "..." # Contents of agent.py above
client.execute(
code=f"import pathlib; pathlib.Path('/home/baponi/control_plane.py').write_text('''{CONTROL_PLANE_PY}''')",
language="python", timeout=10, thread_id=thread,
)
client.execute(
code=f"import pathlib; pathlib.Path('/home/baponi/agent.py').write_text('''{AGENT_PY}''')",
language="python", timeout=10, thread_id=thread,
)
# Run the agent
result = client.execute(
code="python3 /home/baponi/agent.py",
language="bash", timeout=300, thread_id=thread,
)
print(result.stdout)

Every tool call (Write, Read, Bash) hits POST /approve-tool before execution. The sandbox handles isolation; your backend handles policy.

Baponi connectors inject credentials into the sandbox at execution time without exposing them in your code:

ConnectorWhat gets injectedHow Claude Code uses it
AnthropicANTHROPIC_API_KEY env varClaude Code reads it automatically for API calls
GitHub.gitconfig with URL rewriting + GH_TOKEN env vargit clone/push and gh CLI work on private repos transparently

Your wrapper code never touches credentials. Configure them once in the admin UI, attach to your API key, and every execution gets them automatically. See Connectors for supported integrations including PostgreSQL, MySQL, BigQuery, AWS S3, GCS, and more.

For simple tasks (write a function, fix a bug), 300 seconds (5 minutes) is usually enough. For complex multi-file refactors, set up to 3600 seconds (1 hour). The Pro plan supports timeouts up to 3600s.

Can I run multiple Claude Code sessions in parallel?

Section titled “Can I run multiple Claude Code sessions in parallel?”

Yes. Use different thread_id values for each session. Each gets its own persistent filesystem. Baponi handles concurrency, and there’s no shared state between threads.

What happens if Claude Code exceeds the timeout?

Section titled “What happens if Claude Code exceeds the timeout?”

The sandbox terminates the process and returns what was captured so far. Use --max-turns as an additional safety rail to prevent Claude Code from running too many iterations.

The Claude Agent SDK’s bundled binary includes its own internal sandbox runtime (bubblewrap) for local development safety. Inside a Baponi sandbox, the code is already running in an isolated environment with multi-layer security, so the SDK’s internal sandbox is redundant. When it tries to initialize, it triggers low-level operations that conflict with Baponi’s security policy. Setting cli_path to the Node.js CLI (/home/baponi/.local/bin/claude) bypasses this entirely. The Node.js CLI provides the exact same functionality (hooks, streaming, tool approval) without the internal sandbox layer.