Skip to main content
Self-hosted environments are in public beta on Team and Enterprise plans; an Owner or admin enables them by turning on Allow self-hosted environments on the Cloud environments admin page. This page is the flag and metric reference; see the quickstart for setup and Deploy to production for the fleet recipes.
This page is the reference for the two processes you run in a self-hosted environment: the runner, which executes Claude Code cloud sessions on your hosts, and the optional autoscaling orchestrator, which starts runners as sessions queue. Each has its own flag table. Both run on Linux or macOS hosts, which the defaults such as /workspace and ~/.claude assume. Run claude self-hosted-runner --help for the authoritative list on your installed version. Metric series and a few API fields still use pool for what these pages call an environment; both terms name the same thing. The environment ID is the pool_id field, with the form ccpool_...: wherever these pages show a pool identifier, it names the environment. CLI flags and environment variables spell it environment, such as --environment-secret-file; the deprecated pool spellings still work, as the --environment-secret-file row describes.

Runner CLI flags

Most flags have a corresponding environment variable. When both are set, the flag takes precedence. Duration flags take minutes or seconds on the CLI, but the paired environment variable is always in milliseconds, indicated by the _MS suffix, and the Default column shows the flag’s unit: --exit-if-unused-min 10 is equivalent to SELF_HOSTED_RUNNER_IDLE_SHUTDOWN_MS=600000, and a Helm value like SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS: "15" means 15 milliseconds, not the 15-minute default. Most duration flags have a maximum, chosen to keep each timeout inside the runtime’s 32-bit timer ceiling of roughly 24.85 days. The --*-min flags cap at 10080 minutes, 7 days; --drain-grace-sec at 604800 seconds, also 7 days; and --drain-wait-sec at 86400 seconds, 24 hours. --session-stop-grace-sec and --post-session-hook-timeout-sec are uncapped. Overrunning a cap behaves differently per surface:
  • Flag: startup fails with an error.
  • Environment variable: the runner clamps the value to the timer ceiling rather than rejecting it.

Orchestrator CLI flags

The self-hosted-runner orchestrator subcommand, which spawns on-demand runners, accepts --api-url, --environment-secret-file, --hooks-dir, --health-port, and --log-level with the same defaults as the runner and, where the runner’s flag has one, the same environment variable, except that --hooks-dir is required and must contain a spawn-runner hook. It also takes its own flags:

SCM connector flags

The orchestrator can hold a standing WebSocket connection to Anthropic’s control plane so that hosted pre-session flows, such as the repository picker and the branch or ref resolver, can reach a GitHub Enterprise Server host that’s only routable from inside your network. The connector stays off unless you set --scm-connector-host. The connector authenticates with the orchestrator’s existing environment secret and reconnects automatically: with exponential backoff on a dropped connection, or a fixed 30-second delay when the control plane closes the connection because another orchestrator replica already holds it.

Environment-variable-only settings

These runner settings are read from the environment only and cover behavior most deployments leave at the default:

Telemetry

Session children send operational telemetry to Anthropic unless you turn it off. No code or repository content is sent. Set telemetry variables on the runner process; the runner re-asserts them after applying server-provided environment variables, so the operator’s setting always takes precedence. One control is specific to self-hosted environments: CLAUDE_CODE_BYOC_ENABLE_DATADOG=1 opts in to Datadog operational metrics, which are off by default in self-hosted environments. The general Claude Code telemetry controls, DISABLE_TELEMETRY, DO_NOT_TRACK, DISABLE_ERROR_REPORTING, and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, apply to session children as documented in the environment variable reference. DISABLE_GROWTHBOOK is related but different: setting DISABLE_GROWTHBOOK=1 disables feature-flag fetching, and telemetry stays on unless DISABLE_TELEMETRY is also set. CLAUDE_CODE_ENABLE_TELEMETRY is unrelated: it enables OpenTelemetry export to your own collector, as described in Monitoring, and doesn’t control Anthropic’s analytics.

Health endpoint

The runner serves GET /healthz on the configured health port. The response is 200 OK whenever the process is alive, whatever state the poll loop is in, so an HTTP probe on this endpoint detects a dead process only. The JSON body describes current state:
Use last_poll_age_ms as a liveness signal in custom probes; a value that grows unbounded indicates the poll loop is stuck. Both last_poll_at and last_poll_age_ms are null until the first poll completes. The orchestrator serves its own /healthz on its health port. Its endpoint always returns 200, and the body carries a connected field reporting whether the most recent poll succeeded, plus per-state spawn-queue counts in queue_counts. Gate readiness and alerting on connected rather than the status code. When the SCM connector is configured, the orchestrator’s /healthz body also carries scm_connector_connected and a scm_connector object with connected, last_connected_at, last_error, reconnects, and requests_forwarded. Both fields are null when --scm-connector-host isn’t set.

Prometheus metrics

Each runner serves Prometheus metrics at GET /metrics on the same port as /healthz. Key series: The orchestrator serves its own series at GET /metrics on the same port as its /healthz: For autoscaling, pick the series that matches your scaling style and gate it before it feeds the scaler:
  • Queue-depth scaling: feed claude_code_self_hosted_orchestrator_pool_pending_sessions into your HPA or KEDA scaler, not queue_pending_sessions.
  • Capacity scaling: scale on the ratio of the runner’s active_sessions to capacity.
  • Gate on connected: filter the query with claude_code_self_hosted_orchestrator_connected == 1 per instance, so a disconnected replica’s stale value doesn’t feed the scaler.
During a full poll outage, every replica disconnected, the gated query returns no data. HPA holds the current replica count on a missing metric, but KEDA’s Prometheus scaler at its default ignoreNullValues: "true" reads the empty result as zero and scales in; set ignoreNullValues: "false" on the ScaledObject, optionally with a fallback replica floor. The following Prometheus Operator PodMonitor covers both processes. It selects pods by the app.kubernetes.io/part-of: claude-code-self-hosted-runner label and the named health port that the Kubernetes recipe sets; adjust the namespaces to match your deployment:
These sample alert rules are a starting point; tune the thresholds for your fleet size:

Pass through session-child metrics

Each session runs in its own child process with its own OpenTelemetry metrics; at --capacity above one, the runner rewrites how those child metrics are exposed. Setting OTEL_METRICS_EXPORTER=prometheus on the runner host and CLAUDE_CODE_ENABLE_TELEMETRY=1 in the session’s environment, for example from your wrapper script or the runner’s own environment, which sessions inherit, re-exposes each child’s counter and gauge instruments on the runner’s own /metrics endpoint, alongside the runner’s series. The runner rewrites the child’s exporter to push over OTLP to a loopback-only receiver on the health port, tags each series with session_id and client_platform labels, and evicts a session’s series when that session ends. Histograms don’t pass through, and a child metric whose name would collide with the runner’s own prefix is dropped. At the default --capacity 1, the rewrite doesn’t apply: the session’s child binds its own Prometheus endpoint on port 9464 as usual.

Session lifecycle counter semantics

The sessions_started_total, sessions_completed_total, sessions_failed_total, and sessions_interrupted_total counters classify each session by how it ended. Every spawned session child increments sessions_started_total at spawn time, and exactly one of the other three increments at exit, so sessions_started_total minus the sum of the other three equals the number of session children currently running.
  • completed: the session ended cleanly. This covers the child exiting on its own with code 0, the session being archived or deleted while the child was still connected, and the runner releasing the slot as a clean handoff: an idle release, a startup timeout, or a server-side deassign the poll loop noticed before the child exited. Increments sessions_completed_total.
  • failed: the child exited on its own with a non-zero code, either a crash or a setup failure after spawn. Increments sessions_failed_total.
  • interrupted: the runner terminated the child for an operational reason that’s neither a session success nor a runner fault, such as a drain, for example a Kubernetes rolling restart sending SIGTERM, the max-lifetime watchdog --kill-session-after-min, or the released=false backstop: the runner terminates the child after the control plane declines three consecutive idle-release requests, each because a user message was still waiting to be processed. Increments sessions_interrupted_total.
The post-session hook’s CLAUDE_RUNNER_EXIT_REASON doesn’t use this classification for clean handoffs. The hook reports an idle release, a startup timeout, and a server deassign as interrupted, since from the hook’s perspective the runner killed the child, while the counters above record those same events as completed, since nothing went wrong and the slot was handed back cleanly. If you reconcile hook receipts against sessions_completed_total directly, you undercount completions. Use the hook for per-session guarantees and the counters for aggregate rates. On a one-shot environment, --capacity 1 with the default --drain-grace-sec 0, each runner process exits moments after its one session ends. sessions_completed_total, sessions_failed_total, and sessions_interrupted_total increment only at session end, right before that exit, so a Prometheus scrape every 15 to 60 seconds rarely catches the increment before the runner’s series disappears; these three end-of-session counters are the terminal counters the rest of this section refers to. sessions_started_total increments at spawn and stays visible for the life of the session, so it reliably shows up, but on a one-shot environment it reads closer to “sessions currently running” than a cumulative count. Use the series in this table for the corresponding goal instead of the terminal counters: The orchestrator_* rows exist only on environments running the on-demand orchestrator. On a fixed fleet whose runners outlive their sessions, with --drain-grace-sec above 0, use sum(rate(claude_code_self_hosted_runner_sessions_started_total[5m])) for throughput; on a one-shot fleet that series has the same scrape-window problem as the terminal counters, so rely on the queued-sessions count instead. Check backlog on the environment’s Activity tab, on the Cloud environments admin page: the runners don’t export a queue-depth series. For per-session outcome reporting, use the post-session hook instead: it fires at every session end where a child process was spawned, apart from abrupt runner termination such as a VM preemption, per the hook’s own contract.

What’s next