#Hermes
Run Hermes inside a Sandbox0 sandbox when you want Hermes sessions, memory, skills, and gateway state to live on a SandboxVolume while Sandbox0 controls runtime lifecycle and public ingress.
The builtin hermes template uses the upstream Hermes image and declares /opt/data as the persistent mount point.
Create The Sandbox#
Create a SandboxVolume, claim the builtin hermes template, mount the volume, and expose the Hermes API server as a cmd Sandbox Service. The example leaves ttl and hard_ttl unset.
Set one Hermes API key and one Sandbox0 route token before running the SDK examples:
bashexport HERMES_API_SERVER_KEY="$(openssl rand -hex 32)" export HERMES_ROUTE_TOKEN="$(openssl rand -hex 32)"
gopackage main import ( "context" "crypto/sha256" "encoding/hex" "fmt" "log" "os" sandbox0 "github.com/sandbox0-ai/sdk-go" "github.com/sandbox0-ai/sdk-go/pkg/apispec" ) func main() { ctx := context.Background() opts := []sandbox0.Option{sandbox0.WithToken(mustEnv("SANDBOX0_TOKEN"))} if baseURL := os.Getenv("SANDBOX0_BASE_URL"); baseURL != "" { opts = append(opts, sandbox0.WithBaseURL(baseURL)) } client, err := sandbox0.NewClient(opts...) if err != nil { log.Fatal(err) } routeHash := sha256.Sum256([]byte(mustEnv("HERMES_ROUTE_TOKEN"))) volume, err := client.CreateVolume(ctx, apispec.CreateSandboxVolumeRequest{ AccessMode: apispec.NewOptVolumeAccessMode(apispec.VolumeAccessModeRWO), }) if err != nil { log.Fatal(err) } command := "set -eu\n" + "mkdir -p /workspace/.hermes /opt/data\n" + "cp -R /opt/data/. /workspace/.hermes/ 2>/dev/null || true\n" + "rm -f /workspace/.hermes/gateway.lock /workspace/.hermes/gateway.pid\n" + "chown -R hermes:hermes /workspace/.hermes\n" + "sync_back() { mkdir -p /opt/data; cp -R /workspace/.hermes/. /opt/data/ 2>/dev/null || true; }\n" + "/opt/hermes/bin/hermes gateway run --no-supervise &\n" + "child=\"$!\"\n" + "trap 'kill -TERM \"$child\" 2>/dev/null || true; wait \"$child\" 2>/dev/null || true; sync_back; exit 143' TERM INT\n" + "wait \"$child\"\n" + "status=\"$?\"\n" + "sync_back\n" + "exit \"$status\"" sandbox, err := client.ClaimSandbox(ctx, "hermes", sandbox0.WithSandboxBootstrapMount(volume.ID, "/opt/data"), sandbox0.WithSandboxAutoResume(true), sandbox0.WithSandboxEnvVars(map[string]string{ "HERMES_HOME": "/workspace/.hermes", "HERMES_PERSIST_HOME": "/opt/data", "HOME": "/workspace/.hermes", "API_SERVER_ENABLED": "true", "API_SERVER_HOST": "0.0.0.0", "API_SERVER_PORT": "8642", "API_SERVER_KEY": mustEnv("HERMES_API_SERVER_KEY"), }), sandbox0.WithSandboxServices([]apispec.SandboxAppService{ { ID: "gateway", Port: apispec.NewOptInt32(8642), Runtime: apispec.NewOptSandboxAppServiceRuntime(apispec.SandboxAppServiceRuntime{ Type: apispec.SandboxAppServiceRuntimeTypeCmd, Command: []string{"sh", "-lc", command}, }), HealthCheck: apispec.NewOptSandboxAppServiceHealth(apispec.SandboxAppServiceHealth{ Path: apispec.NewOptString("/health"), }), Ingress: apispec.SandboxAppServiceIngress{ Public: true, Routes: []apispec.SandboxAppServiceRoute{ { ID: "gateway", PathPrefix: apispec.NewOptString("/"), Methods: []string{"GET", "POST"}, TimeoutSeconds: apispec.NewOptInt32(60), Resume: true, Auth: apispec.NewOptSandboxAppServiceRouteAuth(apispec.SandboxAppServiceRouteAuth{ Mode: apispec.SandboxAppServiceRouteAuthModeBearer, BearerTokenSHA256: apispec.NewOptString(hex.EncodeToString(routeHash[:])), }), }, }, }, }, }), ) if err != nil { log.Fatal(err) } services, err := sandbox.GetServices(ctx) if err != nil { log.Fatal(err) } for _, service := range services.Services { if service.ID == "gateway" { fmt.Printf("SANDBOX_ID=%s\n", sandbox.ID) fmt.Printf("HERMES_PUBLIC_URL=%s\n", service.PublicURL.Or("")) } } } func mustEnv(key string) string { value := os.Getenv(key) if value == "" { log.Fatalf("%s is required", key) } return value }
Sandbox0 starts Hermes through procd, so the example runs the Hermes binary directly instead of relying on the Docker image entrypoint. Hermes keeps its active runtime home on /workspace/.hermes and syncs it with the persistent /opt/data volume when the process exits.
Use The Public URL#
Call the health endpoint through the returned HERMES_PUBLIC_URL:
bashcurl -fsS "$HERMES_PUBLIC_URL/health" \ -H "authorization: Bearer $HERMES_ROUTE_TOKEN"
Requests that use the Hermes API server should also send the Hermes API server key according to the Hermes API server contract.
Pause And Resume#
Manual pause frees the runtime pod. A later public request can resume the sandbox because the sandbox has auto_resume: true and the route has resume: true.
go_, err := client.PauseSandbox(ctx, sandbox.ID) if err != nil { log.Fatal(err) } _, err = client.ResumeSandbox(ctx, sandbox.ID) if err != nil { log.Fatal(err) }
Hermes state under /opt/data remains on the mounted volume. Running processes, sockets, memory, and in-flight requests are not preserved across pause/resume.
Paused sandboxes cannot receive outbound long-polling, WebSocket, or cron events. Put message webhooks and schedulers in an always-on control plane that calls the Sandbox0 service URL, or keep the sandbox running for workflows that must listen continuously.
Secret Boundary#
Hermes can store model keys and integration secrets in its data directory. For higher-control deployments, keep provider credentials outside the sandbox and route model calls through LLMProxy, egress auth, or another narrow external proxy. The mounted /opt/data volume should hold Hermes state, not broad platform credentials.
Operator Notes#
The builtin template is configurable through Sandbox0Infra.spec.builtinTemplates. Remove templateId: hermes from that list to delete the operator-managed public template, or replace its image, pool, or full spec to pin a reviewed Hermes image.
Next Steps#
OpenClaw
Run OpenClaw from the builtin OpenClaw template.
Volume Mounts
Understand template-declared mount points and claim-time volume binding.