Container orchestration API for spawning and managing isolated Claude Code agents in Podman containers
Stromboli is a REST API that provides secure, isolated execution of Claude AI in containerized environments. It's designed for automating development workflows, CI/CD pipelines, code analysis, and AI-assisted development tasks.
- Isolated Execution: Each Claude instance runs in its own Podman container with resource limits
- Full Claude CLI Support: All 40+ Claude headless mode options exposed via REST API
- Multiple Execution Modes:
- Synchronous execution with immediate response
- Asynchronous execution with job tracking
- Server-Sent Events (SSE) for real-time streaming output
- Session Management: Persistent conversation sessions across multiple API calls
- Webhook Notifications: Get notified when async jobs complete
- Resource Controls: CPU, memory, and timeout limits per container
- Security: Token-based authentication, JWT with refresh tokens, workspace allowlisting, secret management
- Observability: Prometheus metrics, OpenTelemetry distributed tracing, structured logging, request tracking
- Lifecycle Hooks: Run commands at container creation and startup (install deps, start background services)
- Compose Environments: Run Claude in multi-service environments with databases, caches, and other services
- Image Discovery: List, inspect, search, and pull container images via API
- Podman v5+ installed and configured
- Claude credentials (logged in via
claudeCLI)
# 1. Enable Podman socket
systemctl --user enable --now podman.socket
# 2. Setup Claude token (extracts from ~/.claude/.credentials.json)
make claude-setup
# 3. Build images and start
make build-images
make container-start
# 4. Test it works
curl http://localhost:8080/healthThat's it! Stromboli is now running at http://localhost:8080.
For development or when you prefer running directly on the host:
# 1. Configure Claude token
make claude-setup
# 2. Build the agent image
make build-agent-image
# 3. Build and run
make build
./bin/stromboli
# 4. Test it works
curl http://localhost:8080/health# Health check
curl http://localhost:8080/health
# Run Claude synchronously
curl -X POST http://localhost:8080/run \
-H "Content-Type: application/json" \
-d '{
"prompt": "What is the capital of France?",
"claude": {
"dangerously_skip_permissions": true
}
}'| Endpoint | Method | Description |
|---|---|---|
/run |
POST | Execute Claude synchronously (returns output immediately) |
/run/async |
POST | Execute Claude asynchronously (returns job ID) |
/run/stream |
GET | Stream Claude output in real-time via SSE |
| Endpoint | Method | Description |
|---|---|---|
/jobs |
GET | List all async jobs |
/jobs/{id} |
GET | Get job status and result |
/jobs/{id} |
DELETE | Cancel a pending/running job |
| Endpoint | Method | Description |
|---|---|---|
/sessions |
GET | List all sessions |
/sessions/{id} |
DELETE | Destroy session and all its data |
| Endpoint | Method | Description |
|---|---|---|
/auth/token |
POST | Generate JWT access and refresh tokens |
/auth/refresh |
POST | Refresh an expiring access token |
/auth/validate |
GET | Validate a token and return claims |
/auth/logout |
POST | Invalidate a token (logout) |
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check with component status |
/metrics |
GET | Prometheus metrics |
/claude/status |
GET | Check Claude configuration |
Execute Claude and get immediate response:
curl -X POST http://localhost:8080/run \
-H "Content-Type: application/json" \
-d '{
"prompt": "Analyze this Go code for bugs",
"workdir": "/home/user/myproject",
"claude": {
"model": "sonnet",
"dangerously_skip_permissions": true
},
"podman": {
"timeout": "5m",
"memory": "1g",
"cpus": "2"
}
}'Start a long-running task and get notified when complete:
# Start async job
curl -X POST http://localhost:8080/run/async \
-H "Content-Type: application/json" \
-d '{
"prompt": "Refactor this entire codebase for better maintainability",
"workdir": "/home/user/large-project",
"webhook_url": "https://myapp.com/webhook",
"claude": {
"model": "opus",
"dangerously_skip_permissions": true
}
}'
# Response: {"job_id": "job-abc123def456", "session_id": "550e8400-..."}
# Poll for status
curl http://localhost:8080/jobs/job-abc123def456
# Cancel if needed
curl -X DELETE http://localhost:8080/jobs/job-abc123def456Get real-time output via Server-Sent Events:
curl -N http://localhost:8080/run/stream?prompt=Write%20a%20REST%20API%20in%20GoContinue a conversation across multiple API calls:
# First call (creates session)
curl -X POST http://localhost:8080/run \
-H "Content-Type: application/json" \
-d '{
"prompt": "Create a new Go project structure",
"workdir": "/home/user/newproject"
}'
# Response includes: "session_id": "sess-abc123"
# Continue the conversation
curl -X POST http://localhost:8080/run \
-H "Content-Type: application/json" \
-d '{
"prompt": "Now add unit tests for the main package",
"workdir": "/home/user/newproject",
"claude": {
"session_id": "sess-abc123",
"resume": true
}
}'| Variable | Default | Description |
|---|---|---|
STROMBOLI_AUTH_ENABLED |
false |
Enable token-based authentication |
STROMBOLI_API_TOKENS |
- | Comma-separated list of valid API tokens |
STROMBOLI_JWT_SECRET |
- | Secret key for JWT signing (enables JWT auth) |
STROMBOLI_JWT_EXPIRY |
24h |
JWT access token lifetime |
STROMBOLI_RATE_LIMIT_ENABLED |
false |
Enable rate limiting |
STROMBOLI_RATE_LIMIT_RPS |
10 |
Requests per second limit |
STROMBOLI_RATE_LIMIT_BURST |
20 |
Maximum burst size |
STROMBOLI_TRACING_ENABLED |
false |
Enable OpenTelemetry tracing |
STROMBOLI_TRACING_ENDPOINT |
localhost:4317 |
OTLP collector endpoint |
When STROMBOLI_AUTH_ENABLED=true, include a Bearer token:
curl -X POST http://localhost:8080/run \
-H "Authorization: Bearer your-token-here" \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello"}'For production, use JWT tokens with automatic expiration and refresh:
# Set JWT secret to enable JWT auth
export STROMBOLI_JWT_SECRET="your-256-bit-secret"
# Generate JWT tokens using API token
curl -X POST http://localhost:8080/auth/token \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{"client_id": "my-app"}'
# Response: {"access_token": "eyJ...", "refresh_token": "eyJ...", "expires_in": 86400}
# Use JWT for requests
curl -H "Authorization: Bearer eyJ..." http://localhost:8080/run ...
# Logout (invalidate token)
curl -X POST http://localhost:8080/auth/logout \
-H "Authorization: Bearer eyJ..."See docs/AUTHENTICATION.md for complete JWT setup.
Protect your API from abuse by enabling rate limiting:
export STROMBOLI_RATE_LIMIT_ENABLED=true
export STROMBOLI_RATE_LIMIT_RPS=10 # 10 requests per second
export STROMBOLI_RATE_LIMIT_BURST=20 # Allow burst of 20
./bin/stromboliRate limits are applied per IP address. When exceeded, the API returns 429 Too Many Requests. See docs/RATE_LIMITING.md for details.
Enable OpenTelemetry tracing for observability:
export STROMBOLI_TRACING_ENABLED=true
export STROMBOLI_TRACING_ENDPOINT="jaeger:4317"
export STROMBOLI_TRACING_SERVICE_NAME="stromboli-prod"
./bin/stromboliTraces include HTTP requests, runner execution, and internal spans. Compatible with Jaeger, Grafana Tempo, and any OTLP-compatible backend. See docs/CONFIGURATION.md for details.
Control container resources via podman options:
{
"prompt": "...",
"podman": {
"timeout": "10m", // Container timeout (e.g., "5m", "1h")
"memory": "2g", // Memory limit (e.g., "512m", "1g")
"cpus": "2", // CPU limit (e.g., "0.5", "2")
"cpu_shares": 512, // CPU shares (relative weight, default 1024)
"volumes": [ // Additional volume mounts
"/data:/data:ro"
]
}
}Stromboli exposes all Claude CLI options. See docs/API.md for complete reference. Common options:
{
"claude": {
"model": "sonnet", // sonnet, opus, haiku
"session_id": "my-session-id", // Session ID for persistence
"resume": true, // Resume existing session
"continue": true, // Continue latest conversation
"dangerously_skip_permissions": true, // Skip permission prompts (sandboxed only)
"permission_mode": "bypassPermissions", // Permission mode
"system_prompt": "You are a senior dev", // Custom system prompt
"max_budget_usd": 10.0, // Budget limit
"max_turns": 30, // Max agentic turns (0 = unlimited)
"timeout": "30m" // Claude timeout
}
}┌─────────────┐
│ Client │
└──────┬──────┘
│ HTTP/REST
▼
┌─────────────────────────────────┐
│ Stromboli API Server │
│ ┌────────────────────────────┐ │
│ │ Gin HTTP Handlers │ │
│ └───────────┬────────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ Runner (Orchestration) │ │
│ └───────────┬────────────────┘ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ Podman Command Builder │ │
│ └───────────┬────────────────┘ │
└──────────────┼──────────────────┘
│ podman run
▼
┌──────────────────┐
│ Podman Container │
│ (isolated env) │
│ │
│ ┌────────────┐ │
│ │ Claude CLI │ │
│ └────────────┘ │
│ │
│ /workspace │
│ (mounted) │
└──────────────────┘
Key components:
- API Layer: HTTP handlers, authentication, request validation
- Runner Layer: Orchestration logic, execution modes (sync/async/stream)
- Command Builders: Construct Podman and Claude CLI commands
- Job Manager: Track async jobs, cleanup, crash detection, webhook notifications
- Session Manager: Persistent session state across calls
- Secrets Manager: Secure Claude token handling via Podman secrets
For detailed architecture, see docs/ARCHITECTURE.md.
stromboli/
├── cmd/stromboli/ # Application entry point
├── internal/ # Private application code
│ ├── api/ # HTTP handlers, middleware, routes
│ ├── runner/ # Orchestration logic
│ ├── podman/ # Podman command construction
│ ├── claude/ # Claude command construction
│ ├── job/ # Async job management
│ ├── session/ # Session lifecycle
│ ├── auth/ # Authentication
│ └── types/ # Shared types
├── docs/ # Documentation
├── deployments/ # Docker/Podman files
└── tests/ # Integration and E2E tests
# Build binary
make build
# Run tests
make test
# Run tests with coverage
make test-coverage
# Run linter
make lint
# Development with hot reload (requires air)
make dev# Generate all documentation
make docs
# Generate Swagger/OpenAPI spec
make docs-swagger
# Generate Go code documentation
make docs-godoc
# Serve Swagger UI locally
make docs-serve# Run all tests
make test
# Run with coverage report
make test-coverage
open coverage.htmlThe easiest way to deploy Stromboli in production:
# 1. Enable Podman socket
systemctl --user enable --now podman.socket
# 2. Setup Claude token
make claude-setup
# 3. Build and start
make build-images
make container-start
# 4. Verify
curl http://localhost:8080/healthThis uses deployments/docker/compose.yml which handles:
- Podman socket mount - Stromboli talks to host Podman via socket
- Compose-managed secrets - Claude token mounted automatically
- User namespace mapping -
userns_mode: keep-idfor proper permissions - Sessions persistence - Stored in
/tmp/stromboli-sessions
Container commands:
make container-start # Start Stromboli
make container-stop # Stop Stromboli
make container-restart # Restart
make container-logs # View logs
make container-status # Check statusOverride defaults with environment variables:
# Custom resources
export STROMBOLI_RESOURCES_MEMORY="2g"
export STROMBOLI_RESOURCES_CPUS="4"
export STROMBOLI_RESOURCES_TIMEOUT="1h"
# Enable authentication
export STROMBOLI_AUTH_ENABLED="true"
export STROMBOLI_JWT_SECRET="$(openssl rand -base64 32)"
# Enable tracing
export STROMBOLI_TRACING_ENABLED="true"
export STROMBOLI_TRACING_ENDPOINT="jaeger:4317"
# Start with custom config
make container-start# deployments/docker/compose.yml
secrets:
claude-token:
file: ${CLAUDE_SECRETS_FILE:-../../.claude-secrets}
services:
stromboli:
image: stromboli:latest
userns_mode: keep-id # Critical for socket access
ports:
- "8080:8080"
secrets:
- claude-token
volumes:
- ${XDG_RUNTIME_DIR}/podman/podman.sock:/run/podman/podman.sock:ro
- /tmp/stromboli-sessions:/tmp/stromboli-sessions
environment:
STROMBOLI_AGENT_SECRETS_FILE: "/run/secrets/claude-token"
STROMBOLI_AGENT_SESSIONS_DIR: "/tmp/stromboli-sessions"
CONTAINER_HOST: "unix:///run/podman/podman.sock"- Enable Authentication: Always set
STROMBOLI_AUTH_ENABLED=truein production - Workspace Allowlist: Configure allowed workspace paths in
main.go - Resource Limits: Set appropriate CPU/memory limits for containers
- Network Isolation: Run Stromboli in isolated network
- Token Rotation: Regularly rotate API tokens and Claude credentials
- Audit Logging: Enable structured logging and monitor API usage
[Unit]
Description=Stromboli API Server
After=network.target podman.service
[Service]
Type=simple
User=stromboli
WorkingDirectory=/opt/stromboli
ExecStart=/opt/stromboli/bin/stromboli
Environment="STROMBOLI_AUTH_ENABLED=true"
Environment="STROMBOLI_API_TOKENS=token1,token2"
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target- User Guide - Start here! Complete guide for users
- API Reference - Complete API documentation with schemas
- Examples - Practical usage examples
- Architecture - System design and components
- Configuration - All configuration options
- Authentication - JWT and token authentication
- Testing - Testing guide (unit, integration, E2E)
- Vision - Project vision and roadmap
Contributions are welcome! This project follows Test-Driven Development (TDD):
- Write failing tests first
- Implement minimal code to pass
- Refactor while keeping tests green
See CLAUDE.md for development guidelines.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Write tests for your changes
- Implement the feature
- Ensure tests pass (
make test) - Run linter (
make lint) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Pinocchio - Docker-based alternative
- Claude Code - Official Claude CLI tool
- n8n - Workflow automation (see VISION.md for integration)
Built with:
- Gin - HTTP web framework
- Podman - Container engine
- Claude AI - AI assistant
- Prometheus - Monitoring and metrics
Stromboli - Because your CI/CD pipeline deserves an AI volcano 🌋