---
title: "Remote Claude Code Sessions"
description: "Run Claude Code in a sandboxed Baponi environment with automatic credential injection for Anthropic API keys and GitHub access. Clone repos, run headless agent sessions, and push results, all fully isolated."
url: https://baponi.ai/docs/guides/claude-code
lastUpdated: 2026-03-06
---
# 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](/features#connectors). Your code never handles API keys or tokens directly.

## Prerequisites

1. **Baponi account and API key**

   Sign up at [console.baponi.ai](https://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](https://github.com/settings/tokens?type=beta) 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](/features#connectors) for the full list of supported integrations (databases, cloud storage, developer tools).

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

   ```bash
   pip install baponi
   ```

## Quick start: one-shot agent task

Run a single Claude Code task inside a sandbox:

```bash
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
  }'
```

```python
from baponi import Baponi

client = Baponi()

result = client.execute(
    code='claude -p "Research the current price of Bitcoin and summarize your findings" --dangerously-skip-permissions',
    language="bash",
    timeout=300,
)

print(result.stdout)
```

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

## Autonomous GitHub workflow

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

```python
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](/features#connectors).

## Multi-step workflow: clone, agent, push

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.

```python
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.

## Claude Code flags reference

Key flags for headless sandbox execution:

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

## Using the Claude Agent SDK

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 Claude Agent SDK ships a bundled binary with its own internal sandbox runtime. Because the code is already running inside a Baponi sandbox, the SDK's internal sandbox triggers operations that are blocked by Baponi's security policy, and the outer sandbox prevents the inner one from initializing.

Set `cli_path` to the pre-installed Node.js CLI to bypass this. All SDK features (hooks, streaming, tool approval) work identically:

```python
options = ClaudeAgentOptions(
    cli_path="/home/baponi/.local/bin/claude",
)
```

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:

```python title="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:

```python title="agent.py"

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
from claude_agent_sdk.types import (
    AssistantMessage, ResultMessage, TextBlock,
    HookInput, HookMatcher,
)

# 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:

```python
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.

## How connector credential injection works

Baponi [connectors](/features#connectors) inject credentials into the sandbox at execution time without exposing them in your code:

| Connector | What gets injected | How Claude Code uses it |
|-----------|-------------------|------------------------|
| **Anthropic** | `ANTHROPIC_API_KEY` env var | Claude Code reads it automatically for API calls |
| **GitHub** | `.gitconfig` with URL rewriting + `GH_TOKEN` env var | `git 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](/features#connectors) for supported integrations including PostgreSQL, MySQL, BigQuery, AWS S3, GCS, and more.

## FAQ

### What timeout should I set?

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?

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?

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.

### Why does the Agent SDK need `cli_path`?

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.