Skip to content
FunCoding

Search

Search docs, agents, posts, Skills and MCP servers

自托管环境:runner 参考

自托管 runner 与编排器的全部 CLI 标志、仅环境变量设置、遥测、健康端点,以及 Prometheus 指标、PodMonitor 与告警规则示例、会话生命周期计数器语义。

自托管环境目前处于 Team 和 Enterprise 套餐的公开测试阶段,由 Owner 在 Cloud environments 管理页打开 Allow self-hosted environments。本页是你在自托管环境里运行的两个进程的参考:runner(在你的主机上执行 Claude Code 云端会话)和可选的自动伸缩编排器(会话排队时启动 runner)。两者都运行在 Linux 或 macOS 主机上(/workspace、~/.claude 等默认值基于此)。以你安装版本的 claude self-hosted-runner --help 为权威列表。

指标序列和少数 API 字段里仍用 pool 指代本文所说的环境,两者是同一个东西:环境 ID 是形如 ccpool_... 的 pool_id 字段;CLI 标志和环境变量则拼作 environment(如 --environment-secret-file),已弃用的 pool 拼写仍可用。

Runner CLI 标志

多数标志有对应的环境变量;两者都设时以标志为准。时长类标志在 CLI 上用分钟或秒,但配对的环境变量始终是毫秒(带 _MS 后缀),「默认」列显示的是标志的单位:--exit-if-unused-min 10 等价于 SELF_HOSTED_RUNNER_IDLE_SHUTDOWN_MS=600000;而 Helm 值 SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS: "15" 表示 15 毫秒,不是默认的 15 分钟。

标志环境变量默认说明
--api-url <url>无https://api.anthropic.comAPI 基础 URL,仅用于测试时覆盖
--base-dir <path>SELF_HOSTED_RUNNER_BASE_DIR/workspace;Windows 无仓库检出和每会话工作目录所在目录。runner 需要对该路径或其父目录有写权限,启动时创建该目录,无法创建或写入就以 cannot create or write to base directory 退出(v2.1.225 之前是在第一个会话启动时才创建,所以不可用的路径让会话失败而不是启动失败)。Windows 不是受支持的 runner 主机,没有默认值:不传标志或不设变量 runner 启动即退出。同一环境里每个 runner 用相同值
--capacity <n>无1此 runner 处理的最大并发会话数,所有会话属于同一个被锁定的所有者;同一环境里每个 runner 用相同值
--client-label <label>SELF_HOSTED_RUNNER_CLIENT_LABEL主机名runner 注册时发送的标签,同时作为 claude_code_self_hosted_runner_info 的 client_label 标签上报(需要 v2.1.248 及以上)
--configure-gitSELF_HOSTED_RUNNER_CONFIGURE_GIT=1关启动时写入全局 git 身份、启用 Anthropic 提交签名、打开 git 推送协商,并安装追加 Co-authored-by: trailer 的提交 hooks(推送协商需要 v2.1.257 及以上)
--confine-repo-settings <mode>SELF_HOSTED_RUNNER_CONFINE_REPO_SETTINGSwarn护栏模式:当仓库提交的设置试图授予会话自己工作区之外的读写访问、设置环境变量,或覆盖运营者的沙盒/hooks 姿态(如 sandbox.enabled: false、disableAllHooks)时标记该会话。warn 记录违规并仍启动会话,enforce 拒绝该会话,off 关闭扫描
--debug-token-dir <path>SELF_HOSTED_RUNNER_DEBUG_TOKEN_DIR未设把实时令牌写到磁盘以供检查;仅调试用,不要用于生产
--defer-shutdown-max-min <n>SELF_HOSTED_RUNNER_DEFER_SHUTDOWN_MAX_MS0首次 SIGTERM 或 SIGINT 时,继续服务已附着的会话而不是排空它们,N 分钟后释放仍附着的并退出;设置前先调高主机的停止超时。0 禁用(需要 v2.1.238 及以上)
--drain-grace-sec <n>SELF_HOSTED_RUNNER_DRAIN_GRACE_MS0在收到关闭信号或到达退休时间之前,控制活动会话结束后 runner 何时退出:0 立即退出、不再轮询;正数则继续重新轮询锁定所有者的队列那么多秒,代价是失去加固一节所说的每会话容器隔离。对于用 --defer-shutdown-max-min 推迟的首个信号之后,runner 一旦不持有会话就退出,不管这里设什么
--drain-marker-file <path>SELF_HOSTED_RUNNER_DRAIN_MARKER_FILE未设你的主机在发 SIGTERM 前写入的标记文件,用来宣告一次优雅排空。排空开始时该文件存在,runner 就向 Anthropic 报告此次退出是主机排空而不是普通关闭信号;排空本身(包括 --drain-wait-sec 的等待)与不用此标志时相同。请指向会话无法写入的本地文件系统路径(需要 v2.1.271 及以上)
--drain-wait-sec <n>SELF_HOSTED_RUNNER_DRAIN_WAIT_MS0排空开始后(默认在 SIGTERM 时,除非设了 --defer-shutdown-max-min),最多等 N 秒让每个会话进行中的轮次和后台任务完成,再终止子进程。等待期间,runner 把刚结束的后台任务视为仍在运行,直到读取其结果的后续轮次开始,最长为 SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS 窗口
--environment-secret-file <path>SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET必填含环境密钥的文件路径;对于由编排器生成的 runner,则是一次性使用的工单 JWT。SELF_HOSTED_RUNNER_ENVIRONMENT_SECRET 直接携带密钥值,不是文件路径。较旧的 --pool-secret-file 标志和 SELF_HOSTED_RUNNER_POOL_SECRET 变量仍可用并向 stderr 打印弃用提示;早于 2.1.216 的预览版 runner 只认旧名字
--exec-path <path>SELF_HOSTED_RUNNER_EXEC_PATH自身二进制为每个会话生成的二进制或包装脚本
--exit-if-unused-min <n>SELF_HOSTED_RUNNER_IDLE_SHUTDOWN_MS0轮询 N 分钟都从未被分配工作就退出,用于自动伸缩缩容;0 禁用
--git-host-rewrite <from>=<to>无未设克隆前把 https://<from>/... 源 URL 改写为 https://<to>/...,用于分离视图 DNS;可重复,仅标志
--git-ssh-rewrite <host>无未设克隆前把 https://<host>/... 源 URL 改写为 git@<host>:...,用于仅 SSH 的 git 主机;可重复,仅标志
--health-port <port>SELF_HOSTED_RUNNER_HEALTH_PORT8080/healthz 和 /metrics 监听端口;设 0 禁用
--hooks-dir <path>SELF_HOSTED_RUNNER_HOOKS_DIR未设生命周期 hook 脚本目录
--host-config-snapshot <mode>SELF_HOSTED_RUNNER_HOST_CONFIG_SNAPSHOTdiskrunner 把主机配置目录的启动快照放在哪里,每个会话从它播种。disk 把快照复制到 --base-dir 下 runner 自有的目录,并在每个会话开始时对照内存中的摘要校验每个文件;副本中有文件被修改则会话失败,runner 拒绝会话直到重启。memory 把整个快照放在堆里,上限 64 MiB;超过上限时会话不带主机配置启动并显示提示。runner 无法写磁盘快照时记录失败并在该次运行改用 memory
--kill-session-after-min <n>SELF_HOSTED_RUNNER_MAX_LIFETIME_MS0把会话限制在 N 分钟挂钟时间,作为卡死会话的安全限制。v2.1.260 及以上,runner 在会话达到限制时释放它,让它能在用户下一条消息时恢复,仅当 SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS 宽限窗口结束时它仍在 runner 上才终止它;v2.1.260 之前,runner 在限制处直接终止会话。0 禁用
--lock-to-account <id>SELF_HOSTED_RUNNER_LOCK_TO_ACCOUNT未设在启动时把 runner 预先锁定到某个账号,而不是在第一个会话时锁定。接受环境所属组织内的邮箱或 user_... ID;预锁定的 runner 永远不会接 Claude Tag 频道会话(那些会话没有账号)
--log-file <path>SELF_HOSTED_RUNNER_LOG_FILE未设除 stdout 和 stderr 外把 runner 日志镜像到文件,创建时权限为 0600;self-hosted-runner doctor 在本地跟踪日志需要它
--log-level <level>无infoinfo 或 debug
--post-session-hook-timeout-sec <n>SELF_HOSTED_RUNNER_POST_SESSION_HOOK_TIMEOUT_MS60每次会话结束(包括 runner 关闭)时 post-session hook 的时间预算
--proxy-authorization-command <command>SELF_HOSTED_RUNNER_PROXY_AUTHORIZATION_COMMAND未设runner 为每个到出站代理的连接运行的 shell 命令,其去空白后的 stdout 作为 Proxy-Authorization 头的值。需要 HTTPS_PROXY 或 HTTP_PROXY,不能与 --proxy-authorization-file 同用(需要 v2.1.238 及以上)
--proxy-authorization-file <path>SELF_HOSTED_RUNNER_PROXY_AUTHORIZATION_FILE未设runner 为每个到出站代理的连接读取的文件,其去空白后的内容作为 Proxy-Authorization 头的值;适用于由另一个进程原地轮换的令牌。要求与 --proxy-authorization-command 相同,两者不能同用(需要 v2.1.238 及以上)
--push-outcome-on-releaseSELF_HOSTED_RUNNER_PUSH_OUTCOME_ON_RELEASE关在 runner 发起的会话结束(如排空或空闲释放)时,先把跟踪的结果分支推送到 origin 再删除工作区,使进行中的提交能在重启后保留。尽力而为;给关闭预算增加 30 秒,要从已推送的分支恢复需要 git 2.29 或更新。启用前先把推送权限限制到 claude/* 引用;通过 checkout 生命周期 hook 检出的仓库不会被推送,要从 post-session hook 里做快照
--release-idle-session-min <n>SELF_HOSTED_RUNNER_SESSION_IDLE_MS0一个轮次结束后或会话等待用户操作时,闲置 N 分钟后释放会话槽位。仍在轮次中的会话(包括持有永不结束的后台任务或在运行中的工具调用内请求审批的会话)不算空闲,要配合 --kill-session-after-min 作为硬性兜底。会话的后台任务结束后,runner 认为会话忙,直到读取结果的后续轮次开始,最长为 SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS 窗口
--remove-session-state [bool]SELF_HOSTED_RUNNER_REMOVE_SESSION_STATE关会话在该 runner 上结束时(不论结果)删除 <base-dir>/_sessions/ 下它的每会话目录。删除是尽力而为的:runner 被杀或在清理运行前到达排空截止时间时,目录会留着;开启该标志后,失败或被中断的会话的调试日志不会保留在磁盘上(需要 v2.1.268 及以上)
--retire-at <epoch-seconds>SELF_HOSTED_RUNNER_RETIRE_AT未设在一个绝对 Unix 时间戳(秒)让 runner 退休,用于在已知时间杀掉 runner 的基础设施;早于 2001 年或晚于 5138 年的值会被标志拒绝、被环境变量忽略
--session-stop-grace-sec <n>SELF_HOSTED_RUNNER_SESSION_STOP_GRACE_MS5会话结束后等 Claude 进程干净退出多久,之后强制杀掉;子进程自己的 SessionEnd hooks 需要更多时间时调大
--startup-timeout-min <n>SELF_HOSTED_RUNNER_STARTUP_TIMEOUT_MS15子进程在生成后 N 分钟内还没发出已初始化的信号,就释放会话槽位;由子进程在活动通道上的 init 信号清除(普通输出不行),此后由 --release-idle-session-min 接管。0 禁用
--trust-workspace [bool]SELF_HOSTED_RUNNER_TRUST_WORKSPACE开为每个会话的仓库路径预置持久信任,使仓库提交的 permissions.allow 和 additionalDirectories 生效。设为 false 则丢弃仓库提交的权限授予,改在主机配置的 settings.json 里配置允许规则;仓库提交的 sandbox.* 设置无论如何仍然适用,所以仓库设置护栏不管这个标志都会扫描它们
--use-anthropic-git-proxyCLAUDE_RUNNER_USE_GIT_PROXY=1关通过 Anthropic git 代理克隆,而不是客户自管的 git 认证;需要 --capacity 1 和 git 2.32 或更新,否则 runner 拒绝启动;取代改写标志

多数时长标志有上限,取值是为了让每个超时都在运行时 32 位计时器上限(约 24.85 天)之内:--*-min 标志上限 10080 分钟(7 天);--drain-grace-sec 上限 604800 秒(同为 7 天);--drain-wait-sec 上限 86400 秒(24 小时);--session-stop-grace-sec 和 --post-session-hook-timeout-sec 没有上限。超出上限时各接口行为不同:标志启动失败并报错;环境变量则由 runner 钳到计时器上限,而不是拒绝。

编排器 CLI 标志

self-hosted-runner orchestrator 子命令负责生成按需 runner,接受 --api-url、--environment-secret-file、--hooks-dir、--health-port 和 --log-level,默认值与 runner 相同,并且在 runner 的标志有对应环境变量时,用同样的环境变量;不同的是 --hooks-dir 必填且必须包含 spawn-runner hook。它还有自己的标志:

标志默认说明
--hook-concurrency <n>4并行运行的 spawn-runner hook 最大数,同时限制每次轮询认领多少生成请求
--hook-timeout <sec>60这么多秒后终止 hook 的进程树;该超时加上 5 秒的杀进程宽限必须小于 --expected-spawn-seconds,编排器在启动时强制这一点
--expected-spawn-seconds <sec>120生成的 runner 的预期 p99 启动时间,服务端强制的范围是 10 到 3600;每次轮询都作为服务端租约发送,若在此时间内没有 runner 注册,会话会以新的订单 ID 重新提供;所有副本必须共用该值
--min-idle <n>0通过主动生成待命 runner,至少保持 N 个空闲会话槽位;0 禁用预热。配合 runner 的 --exit-if-unused-min,让多余的待命 runner 自行回收
--debug-dir <path>未设把每个生成请求的工单和 hook 的 stderr 写到磁盘;仅调试用,生产中不要设

SCM 连接器标志

SCM 连接器目前不可用,所以不要设置本节的标志。设了 --scm-connector-host 连接不会打开,编排器会一直重试;runner 仍会随会话排队而启动。连接器是编排器到 Anthropic 控制平面的常驻 WebSocket 连接,设计目的是让托管的会话前流程(如仓库选择器和分支/引用解析器)能到达只在你网络内可路由的 GitHub Enterprise Server 主机。

标志默认说明
--scm-connector-host <host[:port]>未设要转发请求到的 GitHub Enterprise Server 主机名,端口默认 443
--scm-connector-id <n>设了 host 时必填你组织的 GitHub Enterprise Server 连接的数字 ID
--scm-connector-provider <slug>ghe标识提供商的路径段,匹配 ^[a-z0-9-]{1,32}$
--scm-connector-ca-file <path>未设到 GitHub Enterprise Server 主机的 TLS 连接用的额外 CA 包(PEM 格式)
--scm-connector-host-rewrite <from>=<to_host:to_port>未设仅用于端到端测试:重定向 TCP 连接,同时保持 Host 头和 TLS SNI 为 --scm-connector-host

每次连接尝试时,编排器发送它已有的环境密钥,并以指数退避自动重试,上限 30 秒加抖动。

仅环境变量的设置

这些 runner 设置只从环境读取,涉及大多数部署保持默认的行为:

环境变量默认说明
SELF_HOSTED_RUNNER_BG_RESULT_GRACE_MS30000后台任务结束后,读取结果的后续轮次还没开始期间,runner 仍把会话视为忙的时长;作用于排空、空闲释放和 --retire-at 退休。0 或不可用值回退到默认,所以无法关闭这个保持(需要 v2.1.228 及以上)
SELF_HOSTED_RUNNER_HOST_CONFIG_DIR~/.claude被捕获到 runner 启动快照并播种到每个会话 CLAUDE_CONFIG_DIR 的目录;磁盘上的改动在 runner 重启后生效。设置该变量还会移动 runner 读取 .claude.json 做 MCP 播种的位置,所以即使设为它自己的默认值也会改变查找位置;指向空目录可完全禁用播种
SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS900000会话达到 --kill-session-after-min 限制后,runner 等待运行中的轮次结束或释放完成多久,再终止会话
SELF_HOSTED_RUNNER_POST_TURN_SETTLE_MS7000轮次结束后,在会话进程向 Anthropic 报告该轮次结束期间,runner 为 --drain-wait-sec 排空把会话计为忙的上限时长。0 或不可用值回退到默认,所以无法关闭(需要 v2.1.275 及以上)
SELF_HOSTED_RUNNER_SIGKILL_GRACE_MS30000runner 等待操作系统向卡在不可中断 I/O 的子进程投递 SIGKILL 多久,之后自己退出。下限为 --post-session-hook-timeout-sec 加 15 秒,设了 --push-outcome-on-release 时再加 30 秒,所以默认下有效最小值是 75 秒
CLAUDE_RUNNER_FETCH_DEPTH50全新克隆的 git fetch 深度;设正整数,或 full/0 表示完整获取。工作区里已有的仓库保持其现有深度
CLAUDE_RUNNER_SKIP_GIT_VERIFY未设为 1 时,跳过 checkout hook 运行后对 .git 是否存在的检查;hook 物化的是非 git 源时设置它
FORCE_AUTOUPDATE_PLUGINS未设为 1 时,即使二进制已固定版本也让插件市场自动更新
CLAUDE_CODE_DISABLE_ARTIFACT未设为 1 时,不论组织的管理员设置如何都在会话里禁用 Artifact 工具,并去掉对 *.frame.claudeusercontent.com 的出站要求

遥测

会话子进程会向 Anthropic 发送运营遥测,除非你关掉;不发送代码或仓库内容。要在 runner 进程上设遥测变量;runner 在应用服务端提供的环境变量之后会重新断言它们,所以运营者的设置总是优先。

自托管环境特有的一个控制是 CLAUDE_CODE_BYOC_ENABLE_DATADOG=1,它选择加入 Datadog 运营指标,该指标在自托管环境里默认关闭。通用的 Claude Code 遥测控制 DISABLE_TELEMETRY、DO_NOT_TRACK、DISABLE_ERROR_REPORTING 和 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 适用于会话子进程,如环境变量参考所述。DISABLE_GROWTHBOOK 相关但不同:设 DISABLE_GROWTHBOOK=1 禁用的是功能标志获取,除非同时设了 DISABLE_TELEMETRY,遥测仍然开着。CLAUDE_CODE_ENABLE_TELEMETRY 与此无关:它启用到你自己收集器的 OpenTelemetry 导出,并不控制 Anthropic 的分析。

健康端点

runner 在配置的健康端口上提供 GET /healthz。只要进程存活,不论轮询循环处于什么状态都返回 200 OK,所以对该端点的 HTTP 探测只能发现进程已死。JSON 正文描述当前状态:

{
  "status": "ok",
  "runner_id": "ccrunner_...",
  "active_sessions": 2,
  "last_poll_at": "2026-03-31T18:04:11.220Z",
  "last_poll_age_ms": 842
}

在自定义探测里把 last_poll_age_ms 当作存活信号;它无限增长说明轮询循环卡住了。第一次轮询完成前,last_poll_at 和 last_poll_age_ms 都是 null。

编排器在它的健康端口上有自己的 /healthz,总是返回 200,正文里的 connected 字段报告最近一次轮询是否成功,queue_counts 给出按状态的生成队列计数;就绪判断和告警要基于 connected 而不是状态码。配置了 SCM 连接器时,编排器的 /healthz 正文还带有 scm_connector_connected 和一个含 connected、last_connected_at、last_error、reconnects、requests_forwarded 的 scm_connector 对象;没设 --scm-connector-host 时两者都是 null。

Prometheus 指标

每个 runner 在与 /healthz 同一端口的 GET /metrics 提供 Prometheus 指标。主要序列:

序列说明
claude_code_self_hosted_runner_info{runner_id,version,client_label}恒为 1;用于机队清点和版本漂移检测
claude_code_self_hosted_runner_capacity配置的 --capacity
claude_code_self_hosted_runner_active_sessions当前运行的会话数
claude_code_self_hosted_runner_locked_account{email}runner 锁定到用户且已签发带 act.email 声明的会话令牌后才出现;锁定到 Claude Tag 智能体的 runner 上不存在该序列(其会话令牌没有 act.email)。标签值是账号邮箱;如果你的指标存储可被广泛读取,请在抓取时丢弃或哈希该标签,例如用 Prometheus 的 metric_relabel_configs
claude_code_self_hosted_runner_last_poll_age_seconds距上次成功轮询的秒数,超过 60 就告警
claude_code_self_hosted_runner_poll_errors_total{error_kind}按类型累计的 PollWork 失败:transport、timeout、5xx、429、4xx;五个序列自进程启动就存在,对 rate(...[5m]) > 0 告警
claude_code_self_hosted_runner_sessions_started_total{client_platform}runner 生命周期内生成的会话子进程数,按会话来源一个序列,如 web_claude_ai、ios、android、desktop_app、claude_code_cli,服务端没发来源时为 unknown;Slack 会话取决于由哪个 Slack 集成创建,带 claude_in_slack 或 claude-in-slack,所以要用正则选择器匹配两者,如 {client_platform=~"claude[-_]in[-_]slack"};用 sum() 得到机队总数
claude_code_self_hosted_runner_sessions_completed_total{client_platform}干净结束的会话,标签相同;范围比单纯的干净退出更宽,见下面的会话生命周期计数器语义
claude_code_self_hosted_runner_sessions_failed_total{client_platform}以失败结束的会话,标签相同,同样有该注意事项
claude_code_self_hosted_runner_sessions_interrupted_total{client_platform}因运营原因而不是会话结果被 runner 终止的会话,标签相同
claude_code_self_hosted_runner_initializing_sessions当前处于初始化阶段的会话数,从分配到子进程的 init 事件
claude_code_self_hosted_runner_session_init_duration_seconds会话初始化耗时直方图
claude_code_self_hosted_runner_session_init_errors_total在到达 init 之前失败的会话:checkout hook 失败、git 准备、令牌问题或 init 前子进程崩溃
claude_code_self_hosted_runner_session_start_hook_errors_total报告错误结果的 SessionStart hooks,每次失败的 hook 执行计一次
claude_code_self_hosted_runner_session_idle_seconds{session_id,client_platform}每会话仪表,自会话变为空闲起的秒数;用于终止卡在未应答权限提示上的会话

编排器在与其 /healthz 同一端口的 GET /metrics 提供自己的序列:

序列说明
claude_code_self_hosted_orchestrator_info{version,pool_id,orchestrator_uuid,hostname}恒为 1
claude_code_self_hosted_orchestrator_connected最近一次轮询成功时为 1;任何失败的轮询(不论类型)之后降为 0
claude_code_self_hosted_orchestrator_last_poll_age_seconds距上次轮询尝试(不论成败)的秒数,不同于 runner 的同名指标(自上次成功起算);要与 connected 配合来发现失败的轮询。编排器的轮询循环要等 hook 执行,所以告警阈值要高于 --hook-timeout 加余量(默认约 90 秒),而不是固定的 60
claude_code_self_hosted_orchestrator_poll_errors_total{error_kind}按类型累计的 PollSpawnHints 失败:transport、timeout、5xx、429、4xx;五个序列自进程启动就存在,对 rate(...[5m]) > 0 告警
claude_code_self_hosted_orchestrator_queue_pending_sessions此刻可认领的生成请求
claude_code_self_hosted_orchestrator_queue_backing_off_sessions在可重试 hook 失败后处于重试退避中的生成请求
claude_code_self_hosted_orchestrator_queue_circuit_broken_sessions被阻塞直到 Owner 在环境的 Activity 标签页重试的生成请求;大于零就告警
claude_code_self_hosted_orchestrator_pool_pending_sessions该环境里等待 runner 的会话总数;是环境范围的聚合值,每个编排器实例上都相同,跨实例用 MAX 而不是 SUM
claude_code_self_hosted_orchestrator_pool_active_sessions该环境里当前分配给存活 runner 的会话数;同样是环境范围的聚合,用 MAX 而不是 SUM
claude_code_self_hosted_orchestrator_spawn_hooks_total{result}累计的 spawn-runner hook 结果:ok、retryable、non_retryable;统计的是编排器 hook 调用,不是 runner 生成的会话子进程,所以与 sessions_started_total 不可比(容量大于一、预热池和为同一会话重新生成的 runner 都会使两者分歧)
claude_code_self_hosted_orchestrator_spawn_hook_duration_secondshook 耗时直方图
claude_code_self_hosted_orchestrator_warm_hints_dispatched_total进程启动以来派发的待命生成请求数
claude_code_self_hosted_orchestrator_session_queue_wait_seconds每个会话在编排器认领它做生成前在队列里等待秒数的直方图,由控制平面随每个会话生成请求发来的排队时间戳记录;用于 p50/p99 排队时间告警;预热生成不被采样
claude_code_self_hosted_orchestrator_clock_skew_seconds本地减服务端的时钟偏差;诊断用,测量到后才出现
claude_code_self_hosted_orchestrator_scm_connector_connectedSCM 连接器的 WebSocket 打开时为 1,拨号或退避时为 0;没设 --scm-connector-host 时不存在
claude_code_self_hosted_orchestrator_scm_connector_requests_forwarded_total进程启动以来代理到所配置 SCM 主机的累计 HTTP 请求数;没设 --scm-connector-host 时不存在

自动伸缩时,选与你的伸缩方式匹配的序列,并在喂给伸缩器之前加门控:按队列深度伸缩把 claude_code_self_hosted_orchestrator_pool_pending_sessions(不是 queue_pending_sessions)喂给 HPA 或 KEDA;按容量伸缩用 runner 的 active_sessions 与 capacity 之比;用 connected 门控,对每个实例用 claude_code_self_hosted_orchestrator_connected == 1 过滤查询,使已断开副本的陈旧值不会喂给伸缩器。整个轮询中断(所有副本都断开)期间,门控后的查询返回无数据:HPA 在指标缺失时保持当前副本数,但 KEDA 的 Prometheus 伸缩器在默认的 ignoreNullValues: "true" 下把空结果读作零并缩容,所以要在 ScaledObject 上设 ignoreNullValues: "false",可选地再配一个 fallback 副本下限。

下面的 Prometheus Operator PodMonitor 覆盖两个进程,它按 app.kubernetes.io/part-of: claude-code-self-hosted-runner 标签和 Kubernetes 配方设置的命名端口 health 选 pod,按你的部署调整命名空间:

# Claude Code 自托管 runner + 编排器的 Prometheus Operator PodMonitor 示例。
# 按你的部署调整命名空间和标签选择器。runner 与编排器都在各自的
# --health-port(默认 8080)上提供 /metrics。
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: claude-code-self-hosted-runner
  namespace: monitoring
spec:
  namespaceSelector:
    matchNames:
      - claude-runners
  selector:
    matchExpressions:
      # 匹配 Kubernetes 配方里的 runner Deployment,以及你以同样方式打标签、
      # 并给了名为 'health' 的 containerPort 的按需 runner Job 和编排器 pod。
      - key: app.kubernetes.io/part-of
        operator: In
        values: [claude-code-self-hosted-runner]
  podMetricsEndpoints:
    - port: health
      path: /metrics
      interval: 30s

下面的告警规则只是起点,阈值要按机队规模调整:

# Claude Code 自托管 runner + 编排器的 Prometheus 告警规则示例。
# 按机队规模和 SLO 调整阈值。
groups:
  - name: claude-code-self-hosted-runner
    rules:
      - alert: ClaudeRunnerPollStale
        expr: claude_code_self_hosted_runner_last_poll_age_seconds > 60
        for: 2m
        labels: {severity: warning}
        annotations:
          summary: "Runner {{ $labels.pod }} has not polled in >60s"
      - alert: ClaudeRunnerVersionDrift
        expr: count(count by (version) (claude_code_self_hosted_runner_info)) > 1
        for: 30m
        labels: {severity: info}
        annotations:
          summary: "Runners are running mixed versions"
      - alert: ClaudeRunnerInitErrorsHigh
        expr: increase(claude_code_self_hosted_runner_session_init_errors_total[10m]) > 3
        for: 5m
        labels: {severity: warning}
        annotations:
          summary: "Runner {{ $labels.pod }}: >3 session init failures in 10m (checkout hook / git / token / pre-init crash)"
      - alert: ClaudeRunnerPollErrors
        expr: sum by (pod) (rate(claude_code_self_hosted_runner_poll_errors_total[5m])) > 0
        for: 2m
        labels: {severity: warning}
        annotations:
          summary: "Runner {{ $labels.pod }}: PollWork failing ({{ $value | humanize }}/s over 5m)"
      - alert: ClaudeRunnerSessionStartHookErrors
        expr: increase(claude_code_self_hosted_runner_session_start_hook_errors_total[10m]) > 3
        for: 5m
        labels: {severity: warning}
        annotations:
          summary: "Runner {{ $labels.pod }}: >3 SessionStart hook failures in 10m"
  - name: claude-code-self-hosted-orchestrator
    rules:
      - alert: ClaudeOrchestratorDisconnected
        expr: claude_code_self_hosted_orchestrator_connected == 0
        for: 2m
        labels: {severity: critical}
        annotations:
          summary: "Orchestrator {{ $labels.pod }} cannot reach the Anthropic control plane"
      - alert: ClaudeOrchestratorPollStale
        expr: claude_code_self_hosted_orchestrator_last_poll_age_seconds > 90
        for: 2m
        labels: {severity: warning}
        annotations:
          summary: "Orchestrator {{ $labels.pod }} has not polled in >90s (poll loop waits on hook execution)"
      - alert: ClaudeOrchestratorCircuitBroken
        expr: claude_code_self_hosted_orchestrator_queue_circuit_broken_sessions > 0
        for: 1m
        labels: {severity: critical}
        annotations:
          summary: "{{ $value }} sessions circuit-broken — spawn-runner hook is repeatedly non-retryable; fix infra then retry from the Activity tab"
      - alert: ClaudeOrchestratorPollErrors
        expr: sum by (pod) (rate(claude_code_self_hosted_orchestrator_poll_errors_total[5m])) > 0
        for: 2m
        labels: {severity: warning}
        annotations:
          summary: "Orchestrator {{ $labels.pod }}: PollSpawnHints failing ({{ $value | humanize }}/s over 5m)"
      - alert: ClaudeOrchestratorSpawnHookFailing
        expr: sum by (pod) (increase(claude_code_self_hosted_orchestrator_spawn_hooks_total{result!="ok"}[5m])) > 3
        for: 5m
        labels: {severity: warning}
        annotations:
          summary: "Orchestrator {{ $labels.pod }}: >3 spawn-runner hook failures in 5m"

透传会话子进程指标

每个会话运行在自己的子进程里,有自己的 OpenTelemetry 指标;--capacity 大于一时,runner 会重写这些子进程指标的暴露方式。在 runner 主机上设 OTEL_METRICS_EXPORTER=prometheus、并在会话环境里设 CLAUDE_CODE_ENABLE_TELEMETRY=1(例如来自你的包装脚本或 runner 自己的环境,会话会继承),就会把每个子进程的计数器和仪表指标重新暴露在 runner 自己的 /metrics 端点上,与 runner 的序列并列。runner 把子进程的导出器改写为向健康端口上仅限环回的接收器经 OTLP 推送,给每个序列打上 session_id 和 client_platform 标签,并在会话结束后清除它的序列。默认的 --capacity 1 下不做这种重写:会话子进程照常在端口 9464 上绑定自己的 Prometheus 端点。

会话生命周期计数器语义

sessions_started_total、sessions_completed_total、sessions_failed_total、sessions_interrupted_total 按会话如何结束来分类。每个生成的会话子进程在生成时让 sessions_started_total 加一,退出时另外三个中恰好一个加一,所以 sessions_started_total 减去另外三者之和等于当前正在运行的会话子进程数。

  • completed:会话干净结束。涵盖子进程自己以代码 0 退出、子进程仍连接时会话被归档或删除,以及 runner 干净地交还槽位:在空闲超时、退休时间或 --kill-session-after-min 限制处释放会话、启动超时,或在子进程退出前轮询循环注意到的服务端取消分配。
  • failed:子进程自己以非零代码退出,崩溃或生成后的设置失败。
  • interrupted:runner 因运营原因终止子进程,既不是会话成功也不是 runner 故障,如排空,或终止在其 --kill-session-after-min 限制后的 SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS 宽限窗口结束时仍在 runner 上的会话。Kubernetes 滚动重启发出 SIGTERM 就是排空的一个例子。

v2.1.260 之前,runner 终止每个达到 --kill-session-after-min 限制的会话,并把它计入 sessions_interrupted_total。post-session hook 的 CLAUDE_RUNNER_EXIT_REASON 对干净交还的分类不同:hook 把释放、启动超时和服务端取消分配报告为 interrupted,因为是 runner 停了子进程;而这些计数器把同样的事件记为 completed,因为槽位是被干净交还的。直接拿 hook 回执去对账 sessions_completed_total 会少算完成数。每会话的保证用 hook,聚合速率用计数器。

在一次性(one-shot)环境,即 --capacity 1 且默认 --drain-grace-sec 0 时,每个 runner 进程在它唯一的会话结束后很快退出。sessions_completed_total、sessions_failed_total、sessions_interrupted_total 只在会话结束、退出之前增加,所以每 15 到 60 秒一次的 Prometheus 抓取很少能在 runner 的序列消失前抓到这次增加(这三个会话结束时的计数器就是本节所说的终态计数器)。sessions_started_total 在生成时增加并在会话期间一直可见,所以可靠地出现,但在一次性环境里它更接近「当前运行的会话」而不是累计吞吐。下表给出对应目标该用的序列:

目标使用
吞吐claude_code_self_hosted_orchestrator_spawn_hooks_total{result="ok"},长期运行的编排器上的计数器,每次成功的 spawn-runner hook 增加一次,在 rate() 下有意义;它统计 hook 调用而不是会话,所以预热和为同一会话重复生成会使它与会话数分歧
利用率sum(claude_code_self_hosted_runner_active_sessions) 对 sum(claude_code_self_hosted_runner_capacity),两个仪表在每次抓取时都有效,不论 runner 寿命
积压队列深度用 claude_code_self_hosted_orchestrator_pool_pending_sessions,以及 claude_code_self_hosted_orchestrator_queue_circuit_broken_sessions,大于零就告警
失败claude_code_self_hosted_runner_sessions_failed_total,尽力而为:生成后的真实崩溃确实会增加它,并且在 --drain-grace-sec 大于 0、runner 比会话活得久时,rate() 有意义;一次性环境有与其他终态计数器相同的抓取窗口问题,所以看到任何非零值都值得调查;生成前的失败(checkout hook 失败、git 准备、令牌问题)只出现在 session_init_errors_total 里

orchestrator_* 行只存在于运行按需编排器的环境。在 runner 比会话活得久(--drain-grace-sec 大于 0)的固定机队上,吞吐用 sum(rate(claude_code_self_hosted_runner_sessions_started_total[5m]));在一次性机队上该序列有与终态计数器相同的抓取窗口问题,所以改靠排队会话数。积压要在 Cloud environments 管理页上该环境的 Activity 标签页查看:runner 不导出队列深度序列。要做每会话结果报告,用 post-session hook:除 VM 被抢占这类 runner 突然终止外,它在每个生成了子进程的会话结束时触发,这是 hook 自己契约的一部分。