SpyBara
Go Premium

self-hosted-environments-deploy.md 2026-11-05 23:03 UTC to 2026-11-06 23:01 UTC

No changes for this document in this history range.

2026
Tue 3 23:58 Mon 9 23:02 Tue 10 21:57 Wed 11 22:59

Развертывание самостоятельно размещаемых окружений в production

Запуск самостоятельно размещаемых runners в production: усиление безопасности, контроль сетевого исходящего трафика, учетные данные git, рецепты Kubernetes и Compose, а также устранение неполадок.

[!NOTE]

Самостоятельно размещаемые окружения находятся в публичной бета-версии на планах Team и Enterprise; раздел Доступность и ограничения охватывает путь включения. Эта страница охватывает запуск флота в production; см. быстрый старт для вашего первого runner и сеанса.

Самостоятельно размещаемое окружение запускает Claude Code облачные сеансы на runners, которые вы развертываете внутри вашей сети, и в production эти сеансы выполняют направленный моделью код от имени всех, кто может отправить сеанс в окружение. Эта страница предназначена для оператора, переводящего рабочее окружение в production. Она проходит через развертывание по порядку: что заблокировать перед подключением реальных систем, исходящий трафик, который требуется флоту, как сеансы аутентифицируются на вашем хосте git, сами рецепты развертывания и что проверить, когда сеансы работают неправильно.

Усиление безопасности развёртывания

Самостоятельно размещаемый runner выполняет произвольный, направленный моделью код на вашей инфраструктуре от имени всех, кто может отправить сессию в его окружение. Это любой член вашей организации Anthropic и любой, кто может запустить сессию канала Claude Tag в области действия, которую Owner направил в окружение. Проработайте каждый элемент перед подключением окружения к production-системам:

  • Эфемерные контейнеры для каждой сессии: запускайте каждый процесс runner в свежем контейнере или VM, который уничтожается при выходе процесса, с --capacity 1 и значением по умолчанию --drain-grace-sec 0, чтобы каждый контейнер обслуживал ровно одну сессию. При более высокой ёмкости или положительном периоде осушения один контейнер обслуживает несколько сессий от одного заблокированного владельца; см. Runner lifecycle. Не переиспользуйте файловую систему между перезапусками runner, кроме намеренной настройки pre-warmed checkout, и никогда между владельцами.

    • Когда runner останавливает сессию, он не отправляет никакого сигнала процессу, который продолжает работать после завершения его shell-команды, например сервису, ставшему демоном. Уничтожение контейнера или VM завершает такой процесс.
  • Нет широких учётных данных в образе: не включайте долгоживущие SSH-ключи, учётные данные облачного провайдера или личные токены доступа, которые предоставляют больше, чем нужно сессии. Создавайте учётные данные, используемые во время сессии, такие как токены push или API, для каждой сессии из вашего скрипта-обёртки. Начальное клонирование происходит перед запуском обёртки, поэтому обрабатывайте его с помощью хука жизненного цикла checkout или с помощью --use-anthropic-git-proxy, если все репозитории сессии находятся на github.com. Для обоих вариантов см. Configure git.

  • Держите учётные данные GitHub хоста вдали от сессий: Claude может использовать любые учётные данные GitHub, которые может прочитать сессия, с любым доступом, который эти учётные данные предоставляют. Не допускайте, чтобы собственные учётные данные GitHub хоста runner с широкими правами попадали туда, где их может прочитать сессия. Такими учётными данными может быть личный токен доступа, токен, который gh auth login сохраняет для вашей учётной записи, или GH_TOKEN в окружении runner.

    • С Anthropic-managed git: при наличии таких учётных данных Claude обращается к GitHub напрямую, а не через Anthropic-managed git.
    • Без Anthropic-managed git: учётные данные для клонирования могут оставаться в образе, если вы ограничите их так строго, как описано в Ship git config in your image.
  • Держите секрет окружения вдали от хостов, запускающих сессии: секрет окружения может регистрировать runners и получать любую сессию, поставленную в очередь в окружение. На фиксированном флоте он находится на каждом хосте runner, где код любой сессии может прочитать файл секрета. Предпочитайте on-demand runners, где секрет остаётся на хосте оркестратора, который никогда не запускает пользовательский код, и каждый runner получает одноразовый наряд на работу, который регистрирует ровно один runner. На фиксированном флоте рассматривайте файл environment-secret как читаемый каждой сессией и ротируйте секрет после любого подозрения на компрометацию сессии.

  • Исходящий трафик по умолчанию запрещён: ограничьте исходящий трафик контейнеров runner и сессии на границе вашей собственной сети в каждом окружении; Default-deny egress описывает, что разрешить и почему.

  • Наименьшие привилегии IAM хоста: вычислительная идентификация, прикреплённая к хосту runner, такая как профиль экземпляра или учётная запись сервиса узла, должна предоставлять только то, что нужно самому runner. Сессии должны получать свои собственные учётные данные через ваш скрипт-обёртку, а не наследовать учётные данные хоста.

  • Блокируйте сессиям доступ к эндпоинту облачных метаданных: чтобы сессии не использовали идентификацию хоста, требуется блокировать их доступ к эндпоинту облачных метаданных, а политики исходящего трафика на уровне подсети не перехватывают трафик метаданных link-local, поэтому блокируйте его в самом контейнере:

    • IMDSv2 с пределом хопов в один
    • GKE Workload Identity со скрытием метаданных
    • Явный запрет для 169.254.169.254 в сетевом пространстве имён контейнера сессии

    Блокировка применяется также к вашему скрипту-обёртке и хукам жизненного цикла, так как они используют тот же контейнер. Аутентифицируйте любой обмен токенами с помощью session JWT в вашем собственном сервисе токенов через разрешённый исходящий трафик или используйте веб-идентификацию на основе файлов, такую как IAM Roles for Service Accounts (IRSA) на Amazon EKS.

  • Изоляция файловой системы для каждого runner: каждый процесс runner получает свой собственный рабочий каталог, который никакой другой процесс на хосте не может читать или записывать. Сделайте --hooks-dir, скрипт-обёртку и ~/.claude/ хоста доступными сессии только для чтения, либо встроив их в образ, либо смонтировав только для чтения.

  • Dispatch не имеет контроля доступа на уровне окружения: любой член вашей организации Anthropic может отправить сессию в любое из её окружений. Если Owner направляет каналы Claude Tag в окружение, любой, кого допускает настройка доступа Claude Tag, может запускать сессии канала, которые работают там. По умолчанию это любой участник подключённого рабочего пространства Slack, с учётной записью Claude или без неё. Рассматривайте каждый хост runner как доступный для выполнения кода всеми, кто может отправлять в него сессии, и размещайте на хосте runner только данные и учётные данные, которые все эти люди имеют право читать. --lock-to-account ограничивает, сессии какой учётной записи выполняет данный хост, но не сужает круг тех, кто может отправлять сессии в окружение. Чтобы сделать самостоятельно размещаемые окружения единственным вариантом выбора, Owner может скрыть окружения, размещаемые Anthropic, для всей организации на странице Cloud environments.

  • Применяйте защиту repo-settings: выберите режим защиты с помощью --confine-repo-settings. Режим по умолчанию warn записывает нарушение в лог и всё равно запускает сессию, enforce отклоняет сессию, а off отключает сканирование. Runner сканирует закоммиченные настройки каждого репозитория на наличие:

    • Гранта, который разрешается за пределами собственного рабочего пространства этой сессии: записи additionalDirectories, правила Edit, Write или NotebookEdit в permissions.allow или записи sandbox.filesystem.allowWrite или allowRead
    • Непустого блока env
    • Переопределения позиции оператора, такого как sandbox.enabled: false

    Защита работает независимо от --trust-workspace и не охватывает хуки репозитория, .mcp.json или правила Bash; см. Permissions and tool approval, чтобы узнать, где должны находиться эти гранты.

[!NOTE]

Если в вашей организации включён список разрешённых IP-адресов, добавьте публичные адреса исходящего трафика ваших runners и контейнеров сессий в список разрешённых перед их запуском. Если вы используете on-demand runners, добавьте также адрес хоста оркестратора. Не полагайтесь на список разрешённых как на сетевой контроль для трафика runner или сессий. Вместо этого применяйте default-deny egress на границе вашей собственной сети.

Требования к сети

Runner и порожденные им дочерние сеансы устанавливают исходящие соединения с хостами ниже. Ограничьте исходящий трафик контейнера сеанса этими хостами и конкретными внутренними сервисами, которые сеансам нужно достичь; Default-deny egress охватывает как и почему.

Эти хосты всегда требуются:

Хост Порт Используется для
api.anthropic.com 443, HTTPS; WSS для git под управлением Anthropic Плоскость управления runner и потоковая передача сессии, вывод модели, флаги функций, аналитика продукта, получение ключей JWKS, подпись коммитов и git под управлением Anthropic при установке --use-anthropic-git-proxy
Ваш git-хост, такой как github.com или ваш хост GitHub Enterprise 443 или 22 Клонирование и push репозиториев на каждом git-хосте, который используют сессии runner. Для runner, использующего --use-anthropic-git-proxy, см. когда путь к github.com всё ещё нужен.

Runner, использующий --use-anthropic-git-proxy, направляет свой git-трафик к github.com через api.anthropic.com, поэтому ему не нужен путь к git-хосту github.com. Этот путь всё ещё нужен, если вы устанавливаете --push-outcome-on-release или выполняете push из хука post-session.

Требуются ли эти хосты, зависит от вашей конфигурации:

Хост Порт Когда требуется
api.github.com 443 С --use-anthropic-git-proxy или без него, когда образ runner включает GitHub CLI и вы передаёте сессиям учётные данные github.com, например GH_TOKEN
downloads.claude.ai 443 Во время установки, когда вы устанавливаете или обновляете Claude Code на хосте с помощью встроенного установщика; сам скрипт install.sh обслуживается с claude.ai. Во время выполнения сеанса только когда сеансы устанавливают плагины из официального рынка Anthropic.
storage.googleapis.com 443 Во время выполнения сеанса для количества установок плагинов и метаданных, показанных в /plugin.
code.claude.com и claude.com 443 Поиск документации встроенным агентом claude-code-guide и предварительно одобренные запросы WebFetch во время сеансов. Блокировка этих хостов влияет только на поиск документации.
*.frame.claudeusercontent.com 443 Только когда инструмент Artifact доступен для сеансов в вашей организации; значения по умолчанию варьируются по плану, согласно таблице доступности там. Установите CLAUDE_CODE_DISABLE_ARTIFACT=1 на runner, чтобы держать инструмент отключенным независимо от параметра организации.
registry.npmjs.org 443 Когда сеанс устанавливает плагин, как для получения пакетов плагинов npm-source, так и для установки зависимостей Node.js плагина, или когда запускается MCP-сервер, запущенный npx
Внутреннее зеркало реестров пакетов, которые используют ваши сборки, или сами эти реестры 443 или порт вашего зеркала Когда сессии устанавливают зависимости, чтобы собрать или протестировать ваши репозитории
http-intake.logs.us5.datadoghq.com 443 Метрики операций Anthropic. Только когда установлено CLAUDE_CODE_BYOC_ENABLE_DATADOG=1; отключено по умолчанию в самостоятельно размещаемых окружениях.
browser-intake-us5-datadoghq.com 443 Загрузки отчетов об ошибках Anthropic, отправляемые только когда отчетность об ошибках включена для учетной записи сеанса. Подавляется DISABLE_ERROR_REPORTING=1 или DISABLE_TELEMETRY=1.
Эндпоинты вашего облачного провайдера для запросов к модели, поиска моделей и обновления учётных данных, такие как bedrock-runtime.us-east-1.amazonaws.com или aiplatform.googleapis.com 443 Только когда runner отправляет запросы к модели в Amazon Bedrock или Google Cloud's Agent Platform

Эти хосты не нужно добавлять в список разрешённых хостов для трафика runner или сессий:

  • statsig.anthropic.com, *.sentry.io, claude.ai и platform.claude.com: эти хосты встречаются в некоторых старых контрольных списках корпоративной сети, но runner к ним не обращается. Получение флагов функций идёт на api.anthropic.com, а runner аутентифицируется с помощью секрета окружения, а не интерактивного OAuth.
  • mcp-proxy.anthropic.com: самостоятельно размещаемые сессии его не используют. Когда доставка коннекторов включена для вашей организации, коннекторы claude.ai вашей организации попадают в сессии через api.anthropic.com. См. MCP servers.

Следующие процессы на стороне хоста обращаются к claude.ai, поэтому запускайте их с хоста, чей исходящий трафик это позволяет, а не расширяйте исходящий трафик контейнера сессии:

  • Однострочный установщик: получает install.sh с claude.ai во время установки.
  • Интерактивный claude auth login: выполняет вход через claude.ai, claude.com и platform.claude.com. Его используют guided setup, режим doctor с входом и CI dispatch. Браузер, в котором вы входите, также загружает браузерные проверки страницы входа claude.ai с hcaptcha.com, *.hcaptcha.com и challenges.cloudflare.com. Если ваша организация использует единый вход (SSO), этот браузер также может быть перенаправлен через sso.claude.com.

Default-deny egress

Развертывайте контейнеры runner и сеанса в сегменте сети или пространстве имен, чей исходящий трафик ограничен хостами в таблице требований к сети, вашим git-хостом и конкретными внутренними сервисами, которые сеансам нужно достичь. Продукт не может проверить или применить это, поэтому применяйте это на границе вашей собственной сети в каждом окружении. Код сеанса направлен моделью и может попытаться подключиться к произвольным хостам; default-deny egress на сетевом уровне ограничивает, где эти попытки могут приземлиться. Это применяется независимо от режима разрешений: набор инструментов, одобренный по умолчанию, уже включает Bash, поэтому исходящий трафик shell работает без подсказки даже без auto mode.

Для деталей о том, какую телеметрию излучает каждый сеанс и как ее отключить, см. Telemetry.

Аутентификация на исходящий прокси

Некоторые корпоративные исходящие прокси требуют заголовок Proxy-Authorization на каждом соединении. Токен в этом заголовке часто ротируется слишком быстро, чтобы писать в URL прокси, который вы устанавливаете в HTTPS_PROXY. Установите HTTPS_PROXY или HTTP_PROXY на URL вашего прокси как обычно, затем установите --proxy-authorization-command или --proxy-authorization-file, чтобы сказать runner, где читать значение заголовка. Оба флага требуют Claude Code v2.1.238 или позже.

Выберите, откуда берется значение Proxy-Authorization

Выберите флаг, который соответствует тому, как вы производите токен Proxy-Authorization:

  • --proxy-authorization-command <command>: выберите это для токена, который вы генерируете по требованию. Runner запускает команду shell и использует ее обрезанный stdout как значение заголовка, например Bearer <token>.
  • --proxy-authorization-file <path>: выберите это для токена, который другой процесс ротирует на месте. Runner читает файл и использует его обрезанное содержимое как значение заголовка.

Конфигурации, с которыми runner отказывается запускаться

Каждый флаг также имеет форму переменной окружения, указанную рядом с ним в справочнике флагов CLI runner. Перед тем как runner свяжется с вашим прокси или плоскостью управления, он проверяет флаги и их переменные и отказывается запускаться в трех случаях:

  • Оба флага установлены: один флаг плюс переменная окружения другого флага считается установкой обоих.
  • Нет URL прокси: ни HTTPS_PROXY ни HTTP_PROXY не содержат URL http:// или https://. Runner читает обе переменные в верхнем или нижнем регистре и не консультирует ALL_PROXY.
  • Любой флаг передан подкоманде orchestrator: self-hosted-runner orchestrator не принимает флаги или их переменные окружения. Вместо этого передайте флаг каждому runner, который запускает оркестратор.

Что runner изменяет, пока установлен флаг proxy-authorization

С установленным любым флагом runner запускает собственный слушатель и отправляет трафик прокси от себя, своих lifecycle hooks и своих сеансов через этот слушатель. Слушатель добавляет заголовок Proxy-Authorization на пути к вашему прокси.

  • Слушатель: слушатель является forward proxy на 127.0.0.1. Runner запускает слушатель перед регистрацией в плоскости управления и выходит при запуске, если слушатель не может запуститься.
  • Переменные прокси: runner переписывает любой из HTTPS_PROXY и HTTP_PROXY, который вы установили, чтобы он указывал на слушатель. Это переписанное значение достигает самого runner, его lifecycle hooks и каждого сеанса, который он запускает.
  • Ротация токена: ротированный токен вступает в силу без перезагрузки. Для каждого соединения, которое слушатель открывает для вашего прокси, runner запускает вашу команду или читает ваш файл снова и добавляет результат как заголовок.
  • Окружение сеанса: сеанс достигает вашего прокси только через слушатель. В окружении каждого сеанса runner удаляет ALL_PROXY, удаляет любое написание HTTPS_PROXY или HTTP_PROXY, которое вы не установили, и закрепляет NO_PROXY на собственное значение runner.
  • Логи: runner никогда не регистрирует значение заголовка.

Настройка git

Раннер управляет извлечением репозиториев, но по умолчанию не настраивает идентичность git и учётные данные. Вы контролируете образ раннера и окружение его процесса, а значит, и конфигурацию git. Выберите один из двух подходов:

  • Позволить раннеру настроить git: запустите раннер с --configure-git, чтобы он записал ту же конфигурацию идентичности и подписи коммитов, что используется в сессиях, размещённых Anthropic
  • Поставлять конфигурацию git в образе: задайте идентичность и учётные данные для push самостоятельно, например чтобы делать коммиты от имени собственного бота

Для репозиториев на github.com также можно запустить раннер с --use-anthropic-git-proxy или задать CLAUDE_RUNNER_USE_GIT_PROXY=1, чтобы попросить Anthropic обслуживать git для сессий раннера.

Минимальные версии Git на хосте раннера: подпись коммитов по SSH с --configure-git требует Git 2.34 или новее, --use-anthropic-git-proxy требует 2.32 или новее, а возобновление сессий из веток, отправленных с помощью --push-outcome-on-release, требует 2.29 или новее. Git 2.24 достаточно, если вы не используете ни одну из этих трёх возможностей и управляете идентичностью git самостоятельно.

Позволить раннеру настроить git

Запустите раннер с --configure-git или задайте SELF_HOSTED_RUNNER_CONFIGURE_GIT=1, чтобы при запуске он записал глобальную конфигурацию git:

  • user.name = Claude и user.email = noreply@anthropic.com, как в сессиях, размещённых Anthropic
  • Подпись коммитов и тегов в формате SSH через управляемую раннером прослойку, которая подписывает каждый коммит через сервис подписи Anthropic с использованием собственных учётных данных сессии. Подписи проверяются на GitHub по опубликованному ключу подписи SSH от Anthropic.
  • push.negotiate = true, чтобы перед упаковкой push git спрашивал у вашего git-хоста, какие коммиты у него уже есть. Требуется Claude Code v2.1.257 или новее.
  • core.hooksPath, указывающий на управляемый раннером каталог хуков. Его хуки commit-msg и prepare-commit-msg добавляют к каждому коммиту трейлер Co-authored-by: для создателя сессии. Трейлер формируется из адреса электронной почты в CCR_SESSION_ACCOUNT_EMAIL и опускается, если эта переменная не задана. Если ваш образ уже задаёт core.hooksPath и раннер не использует управляемый Anthropic git, раннер сохраняет вашу настройку, пропускает установку этих хуков и выводит предупреждение [runner:git].

Подпись коммитов требует git 2.34 или новее; раннер проверяет это при запуске и завершается с ошибкой, если ваш git старее. Этот флаг не настраивает учётные данные для push — их по-прежнему нужно предоставить в образе.

На раннере версии v2.1.280 или новее коммиты, которые вы делаете из хука жизненного цикла checkout или post-session, также подписываются от имени сессии, но без трейлера Co-authored-by:. В разделе Конфигурация git внутри хуков жизненного цикла описаны настройки git, которые раннер фиксирует внутри этих хуков.

С --configure-git или без него Claude Code указывает Claude завершать сообщения коммитов трейлером Claude-Session: <url>, а описания pull request — URL сессии. Чтобы исключить и то и другое, задайте attribution.sessionUrl значение false в файле ~/.claude/settings.json на хосте раннера, а затем перезапустите раннер.

Поставлять конфигурацию git в образе

Идентичность git необходима для любого коммита. Задайте её на уровне системы в Dockerfile, чтобы конфигурация применялась независимо от того, от имени какого пользователя работает процесс раннера:

RUN git config --system user.name "Claude" && \
    git config --system user.email "noreply@anthropic.com"

Без идентичности git commit завершается ошибкой Please tell me who you are, и сессии не могут продвинуться. Вместо этого можно использовать идентичность собственного бота; раннер не переопределяет эти значения.

Не встраивайте долгоживущие учётные данные для push или учётные данные с широкими правами в общий образ раннера: учётные данные в образе доступны каждой сессии, которую запускает этот образ, кто бы её ни создал. Вместо этого выпускайте для каждой сессии краткоживущий токен с минимально необходимыми правами из своего скрипта-обёртки, используя идентичность создателя сессии, декодированную из JWT сессии. Сочетайте это с эфемерным контейнером для каждой сессии, что требует --capacity 1, чтобы никакие учётные данные не пережили сессию, для которой они были выпущены; см. раздел об усилении защиты.

Если необходимо настроить учётные данные для push на уровне образа, например для ключа развёртывания только для чтения, ограничьте их настолько жёстко, насколько позволяет ваш git-хост:

  • Ключ развёртывания SSH, ограниченный одним репозиторием, с перезаписью url.<base>.insteadOf
  • credential.helper, возвращающий токен с минимальной областью действия
  • GIT_SSH_COMMAND, указывающий на ключ с узкой областью действия

Какой бы механизм вы ни настроили, он должен работать без интерактивного запроса, поскольку встроенные клонирование и fetch раннера отключают запросы, которые иначе показали бы git, SSH и Git Credential Manager:

  • Раннер задаёт GIT_TERMINAL_PROMPT=0, поэтому git не запрашивает имя пользователя или пароль.
  • Раннер запускает SSH с BatchMode=yes, добавленным к вашему GIT_SSH_COMMAND, если вы его задали, поэтому SSH не запрашивает парольную фразу или подтверждение хоста.
  • Раннер задаёт GCM_INTERACTIVE=never, поэтому Git Credential Manager не открывает диалог входа. До v2.1.229 подготовка репозитория могла зависнуть на запросе Git Credential Manager.
  • Раннер очищает core.askPass, поэтому, если вы используете askpass-помощник, задайте его через переменную окружения GIT_ASKPASS.

Если ваш git-хост отклоняет учётные данные или вы их не настроили, раннер делает несколько повторных попыток, а затем прерывает подготовку репозитория, если это репозиторий, в который сессия отправляет результаты. О том, когда раннер вместо этого пропускает репозиторий, который сессия только читает, рассказано в разделе Устранение неполадок. Раннер не передаёт эти настройки в окружение сессии.

Храните любую программу, указанную в GIT_SSH_COMMAND или GIT_ASKPASS, там, куда сессии не могут писать, — так же, как контрольный список усиления защиты требует для каталога хуков и скрипта-обёртки. То же относится к любому ключу или файлу в командной строке этой программы. Собственный git раннера запускает эту программу при клонировании или fetch.

Если каталоги извлечения принадлежат другому uid, чем процесс раннера, git отказывается с ними работать; добавьте safe.directory:

RUN git config --system --add safe.directory '*'

Использование git-прокси Anthropic

С git-прокси Anthropic, также называемым управляемым Anthropic git, образу раннера не нужны ключи SSH, помощник учётных данных, .netrc или другие учётные данные git для самой сессии. Вместо этого раннер просит Anthropic обслуживать git для своих сессий. Для пользовательской сессии, которую обслуживает Anthropic, клонирование раннера и собственные fetch и push сессии проходят через Anthropic, которая использует OAuth-токен GitHub, сохранённый для создателя сессии. Сессии ботов и агентов описаны в разделе Как Anthropic обслуживает git для сессии.

Git-прокси отключён, пока вы не включите его. Раннеру, который обращается к вашему git-хосту со своими учётными данными, он не нужен, и его git работает с любым git-хостом.

Взамен git-прокси ограничивает то, что поддерживает раннер, и меняет то, что ему нужно:

  • Только github.com: Anthropic обслуживает сессию, только если все её репозитории находятся на github.com, а git-прокси пока не поддерживает GitHub Enterprise Server. На раннере с git-прокси сессия с репозиторием на другом git-хосте не запускается.

  • Учётные данные только для репозиториев сессии: Anthropic предоставляет учётные данные git для репозиториев, входящих в сессию, но не для других репозиториев на том же git-хосте. Приватный подмодуль, зависимость, которую ваш менеджер пакетов получает через git, или маркетплейс плагинов в другом репозитории не получают учётных данных от Anthropic. Попросите тех, кто создаёт сессии, добавлять каждый репозиторий, который нужен сессии, при её создании.

    Каждый запуск облачной запланированной задачи — это сессия с репозиториями задачи, поэтому попросите владельца каждой задачи добавить все репозитории, которые нужны её запускам.

  • Только push веток: push, удаляющий ветку, завершается ошибкой, как и push в ref любого другого типа, например в тег. О том, какие ветки может обновлять push, см. GitHub-прокси.

  • Подключённые аккаунты GitHub: попросите каждого, кто создаёт пользовательские сессии с репозиторием, подключить GitHub. Без собственного подключения такие сессии не запускаются.

  • --capacity 1: git-прокси требует одну сессию на процесс раннера, поэтому для параллелизма запускайте больше реплик. Требования перечислены в разделе Включение git-прокси Anthropic.

  • Заменённая глобальная конфигурация git: раннер удаляет и заменяет глобальную конфигурацию git пользователя, от имени которого работает. Запускайте его от имени выделенного пользователя или в контейнере.

  • Учётные данные хоста для push с хоста: push раннера через --push-outcome-on-release и любой push, который делает ваш хук post-session, по-прежнему используют собственные учётные данные git хоста раннера и его сетевой путь к github.com. Не храните эти учётные данные в глобальной конфигурации git; способы их предоставления см. в разделе Поставлять конфигурацию git в образе.

  • Решение для каждой сессии: Anthropic решает для каждой сессии на раннере, обслуживать ли её git, и сессия, которую она не обслуживает, не запускается. Причины описаны в разделе Когда сессии не запускаются на раннере с git-прокси.

[!WARNING]

При заданном --use-anthropic-git-proxy раннер удаляет и заменяет глобальную конфигурацию git пользователя, от имени которого работает, не сохраняя резервной копии. Он делает это при запуске и перед каждой сессией. Помощник входа или учётных данных, который вы там хранили, будет потерян. Настройки, которые записывает --configure-git, сохраняются. Запускайте раннер от имени выделенного пользователя или в контейнере, но никогда от своего собственного пользователя.

Храните несекретные настройки git, такие как идентичность и safe.directory, в системной конфигурации git.

Включение git-прокси Anthropic

Прежде чем запускать раннер с --use-anthropic-git-proxy, убедитесь, что хост раннера соответствует каждому из этих требований. Раннер отказывается запускаться, если требование к capacity или git не выполнено:

  • Claude Code v2.1.267 или новее: более ранние версии принимают флаг, но не сообщают запрос на обслуживание git Anthropic и не выводят строку Registering as opted in, поэтому Anthropic не обслуживает их сессии.
  • --capacity 1, значение по умолчанию: каждый процесс раннера обрабатывает одну сессию за раз, поэтому для параллелизма запускайте больше реплик.
  • Git 2.32 или новее: более старый git игнорирует посессионную конфигурацию git, которую раннер настраивает для git-прокси.

[!WARNING]

Рецепты Kubernetes и Docker Compose на этой странице используют --capacity 4. Если вы добавите --use-anthropic-git-proxy или CLAUDE_RUNNER_USE_GIT_PROXY=1 в один из них, не изменив capacity на 1, раннер будет завершаться при запуске каждый раз, когда оркестратор его перезапускает. Задайте --capacity 1 и для параллелизма запускайте больше реплик. Строка, которую выводит раннер, показана в разделе Когда раннер завершается.

Чтобы включить git-прокси, добавьте --use-anthropic-git-proxy в команду раннера или задайте CLAUDE_RUNNER_USE_GIT_PROXY=1 в окружении раннера. Эта команда, выполненная в оболочке на хосте раннера, запускает раннер из быстрого старта с включённым git-прокси:

claude self-hosted-runner --environment-secret-file '/etc/claude/environment-secret' --base-dir '<writable-dir>' --use-anthropic-git-proxy

При запуске раннер выводит Registering as opted in to Anthropic-managed git (--use-anthropic-git-proxy). Затем Anthropic решает для каждой сессии на этом раннере, обслуживать ли её git. Для каждой обслуживаемой сессии раннер записывает в лог строку [runner:session], содержащую governed git ACTIVE. Если вместо этого сессия не запускается, см. Когда сессии не запускаются на раннере с git-прокси.

Как Anthropic обслуживает git для сессии

Для сессии, которую обслуживает Anthropic, клонирование раннера и собственные fetch и push сессии проходят через Anthropic с аутентификацией собственным краткоживущим токеном сессии:

  • Пользовательские сессии: Anthropic использует OAuth-токен GitHub, сохранённый для создателя сессии.
  • Сессии ботов и агентов: Anthropic использует токен установки GitHub App вашей организации.
  • Перезапись URL: --git-host-rewrite и --git-ssh-rewrite не действуют на репозиторий, который обслуживает git-прокси.

Когда сессии не запускаются на раннере с git-прокси

На раннере, запущенном с --use-anthropic-git-proxy, сессия не запускается, если Anthropic не обслуживает её git. Найдите в логе раннера ошибку git, в которой указан адрес api.anthropic.com, содержащий /git_proxy/.

Для каждой сессии раннер на Claude Code v2.1.267 или новее также записывает в лог либо строку [runner:session], содержащую governed git ACTIVE, если Anthropic обслуживает git сессии, либо одну строку [runner:warn], содержащую the server withheld Anthropic-managed git for this session, если нет. Найдите среди этих случаев строку, которую вы видите:

  • Нет ни governed git ACTIVE, ни строки withheld: раннер старее Claude Code v2.1.267 не записывает ни одну из этих строк, и Anthropic не обслуживает его сессии. Обновите раннер до v2.1.267 или новее, следуя разделу Закрепление версии.
  • Строка withheld: Anthropic не стала обслуживать сессию. Раннер, который раньше работал с git-прокси, может так отказать без каких-либо изменений с вашей стороны.
    • Репозиторий не на github.com: сессия хотя бы с одним репозиторием на другом git-хосте, например GitHub Enterprise Server, не обслуживается, включая её репозитории на github.com. Отключите git-прокси Anthropic для раннеров этого окружения.
    • Все репозитории на github.com: сообщите о сбое вашей команде по работе с аккаунтом Anthropic, указав ID сессии из строки withheld. Anthropic фиксирует причину на своей стороне.
  • Строка, содержащая remote: access denied by the git proxy: сессии, которую обслуживает Anthropic, всё равно может быть отказано, например когда политика организации запрещает доступ к git для сессии или сессия не авторизована для репозитория. Тогда в логе раннера появляется строка, содержащая remote: access denied by the git proxy, а остальная часть этой строки объясняет причину.
  • GitHub authentication required: появляется, когда у создателя сессии нет рабочего подключения GitHub на claude.ai. Клонирование сессии завершается ошибкой, и ошибка git содержит GitHub authentication required. Please reconnect your GitHub account. Попросите этого человека подключить или переподключить GitHub в настройках claude.ai.

После устранения причины снова запустите сессии, которые не запустились.

Отключение git-прокси Anthropic

Если сессии в окружении используют репозиторий на git-хосте, отличном от github.com, например GitHub Enterprise Server, отключите --use-anthropic-git-proxy для раннеров этого окружения.

1. Удалите флаг

Удалите --use-anthropic-git-proxy из команды раннера. Если вы задали CLAUDE_RUNNER_USE_GIT_PROXY в окружении раннера, например в спецификации pod или файле Compose, удалите её оттуда. В оболочке сбросьте её:

unset CLAUDE_RUNNER_USE_GIT_PROXY

2. Предоставьте раннеру учётные данные git

Предоставьте учётные данные, работающие без интерактивного запроса, для каждого git-хоста, который используют сессии раннеров, включая github.com. Любые учётные данные, которые были в глобальной конфигурации git пользователя раннера, утрачены, поскольку раннер удалил эту конфигурацию, пока был задан --use-anthropic-git-proxy. Поставляйте учётные данные в образе или используйте хук жизненного цикла checkout.

3. Откройте сетевой путь

Разрешите раннеру доступ к каждому git-хосту, который используют сессии раннеров, по порту 443 или 22. См. строку о git-хосте в разделе Сетевые требования.

4. Перезапустите раннеры

Перезапустите раннеры, чтобы они зарегистрировались без git-прокси. Затем снова запустите каждую сессию, которая не запустилась.

Доступ к API GitHub без GitHub CLI

Если ваш образ раннера не включает GitHub CLI, Claude Code может предоставить встроенный gh, чтобы Claude всё равно мог открывать pull request, оставлять комментарии и читать результаты CI. Встроенный gh предназначен для раннеров, использующих управляемый Anthropic git. Он поддерживает одну команду, gh api, которая вызывает REST API GitHub. Требуется Claude Code v2.1.287 или новее в образе раннера.

Эта команда открывает pull request вместо gh pr create. Встроенный gh подставляет {owner} и {repo} для текущего репозитория:

gh api repos/{owner}/{repo}/pulls -f title='Fix' -f head='my-branch' -f base='main'
  • Учётные данные: встроенный gh отправляет свои REST-запросы через управляемый Anthropic git, который предоставляет учётные данные GitHub на стороне Anthropic, поэтому образу не нужен токен GitHub для этого
  • Какие сессии его получают: Anthropic решает для каждой сессии, обслуживает ли управляемый Anthropic git gh этой сессии. Если да, строка [runner:session] governed git ACTIVE, которую раннер записывает в лог для сессии, показывает gh_path_shim=true. Если нет, у сессии нет gh
  • jq: установите jq в образ, если хотите, чтобы работал --jq
  • CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: если окружение сессии задаёт эту переменную, Claude Code не предоставляет встроенный gh, и у сессии нет gh

Доступ к API GitHub с GitHub CLI

Если ваш образ раннера включает GitHub CLI для использования сессиями, пусть ваш скрипт-обёртка выпускает для каждой сессии краткоживущий GH_TOKEN с минимально необходимыми правами для создателя сессии. Если gh находится в PATH, с которым запускается Claude Code, у сессии есть этот gh и нет встроенного gh. С этим токеном такой gh обращается к GitHub с тем доступом, который предоставляет токен, и в обход управляемого Anthropic git, поэтому разрешите доступ к api.github.com.

При использовании управляемого Anthropic git не включайте GitHub CLI в образ, чтобы вместо него использовался встроенный gh.

Доверие частному центру сертификации при использовании управляемого Anthropic git

Этот раздел применим, если вы задаёте GIT_SSL_CAINFO или GIT_SSL_NO_VERIFY в окружении раннера, сессии которого используют управляемый Anthropic git. Описанная обработка требует, чтобы раннер работал на Claude Code v2.1.283 или новее.

Когда git на раннере должен доверять частному центру сертификации (CA), например тому, которым подписывает прокси с инспекцией TLS, обычные подходы работают следующим образом:

  • Системное хранилище сертификатов: установите свой CA в системное хранилище сертификатов хоста раннера, и git будет доверять ему без каких-либо из этих переменных.
  • GIT_SSL_CAINFO: задайте путь к PEM-файлу с вашими CA, например GIT_SSL_CAINFO=/etc/ssl/corp-ca.pem.
  • GIT_SSL_NO_VERIFY: не помогает за повторно подписывающим прокси. Собственное клонирование раннера через управляемый Anthropic git проверяет сертификаты, даже если эта переменная задана, поэтому такое клонирование завершается ошибкой, пока git не начнёт доверять вашему CA одним из двух других способов.

Для git-соединений, передающих токен сессии в управляемый Anthropic git, раннер применяет эти две переменные следующим образом. Хук command запускается с окружением сессии, поэтому получает то же, что и git внутри сессии:

  • GIT_SSL_CAINFO: то, с чем git сверяет управляемый Anthropic git, зависит от того, где запускается git:
    • Собственные клонирование и fetch раннера: выполняются без переменной и сверяют управляемый Anthropic git с посессионным файлом сертификатов, который записывает раннер. Раннер собирает этот файл из системного пакета CA хоста раннера и сертификатов из вашего файла.
    • Git внутри сессии: получает конфигурацию http.sslCAInfo с указанием вашего файла вместо переменной, а также записи http.<url>.sslCAInfo, которые сверяют управляемый Anthropic git с посессионным файлом.
    • Хуки checkout и post-session: наследуют переменную без изменений.
  • GIT_SSL_NO_VERIFY: какие проверки сертификатов остаются отключёнными, зависит от того, где запускается git:
    • Собственные клонирование и fetch раннера: выполняются без переменной и проверяют предъявленный им сертификат.
    • Git внутри сессии: получает конфигурацию http.sslVerify=false вместо переменной, поэтому проверки остаются отключёнными для других хостов. Он также получает записи http.<url>.sslVerify=true, которые сохраняют проверки включёнными для управляемого Anthropic git.
    • Хуки checkout и post-session: если у сессии есть репозиторий на управляемом Anthropic git, получают конфигурацию http.sslVerify=false вместо переменной. Они также получают записи http.<url>.sslVerify=true, которые сохраняют проверки включёнными для управляемого Anthropic git.

Для посессионного файла сертификатов нужен системный пакет CA по пути /etc/ssl/certs/ca-certificates.crt или /etc/pki/tls/certs/ca-bundle.crt на хосте раннера. Также нужен файл GIT_SSL_CAINFO, который может прочитать пользователь раннера, который содержит PEM-блоки CERTIFICATE и размер которого не превышает 1 МиБ. Если раннер не может собрать посессионный файл, он записывает в лог строку [runner:warn], содержащую did not build the certificate file и причину. Тогда git использует ваш файл как есть для управляемого Anthropic git. Исправьте то, что указано в строке.

Для каждой сессии, использующей управляемый Anthropic git, раннер также записывает в лог строку [runner:warn], начинающуюся с governed git: GIT_SSL_CAINFO is set или governed git: GIT_SSL_NO_VERIFY is set. В строке сказано, что раннер сделал с этой переменной для собственного git, для git внутри сессии и для ваших хуков жизненного цикла. Она завершается указанием, нужно ли вам что-либо менять.

Добавление репозиториев github.com в сессии каналов Claude Tag

Если Claude не может добавить репозиторий github.com в сессию канала Claude Tag, убедитесь, что у канала есть к нему доступ, даже если ваш раннер может его клонировать. Этот доступ определяется настройками на GitHub и в вашей организации Claude, а не учётными данными git вашего раннера.

1. Установите Claude GitHub App

Установите Claude GitHub App в организацию или аккаунт GitHub, которому принадлежит репозиторий, и включите этот репозиторий в установку.

2. Свяжите организацию или аккаунт GitHub

В разделе Organization settings > Git providers свяжите эту организацию или аккаунт со своей организацией Claude. Связать её может Owner вашей организации Claude, который также является владельцем организации GitHub. См. Связывание организации GitHub.

3. Предоставьте доступ к репозиторию

В настройках администратора Claude Tag предоставьте репозиторий каналу или более широкой области действия, включающей канал: его рабочему пространству Slack или всему Slack. Предоставить его может пользователь с ролью Owner или разрешением Claude Tag Admin. Менеджер канала также может добавить его в свой канал, если его аккаунт GitHub является администратором репозитория. См. Предоставление доступа к репозиторию.

Затем начните новую ветку обсуждения в канале и назовите репозиторий в первом сообщении.

Если Claude по-прежнему не может добавить репозиторий, см. Если Claude не может получить доступ к репозиторию, где описано, как проверить связь с GitHub и предоставление доступа к репозиторию.

Перезапись URL git для частных сетей

URL репозиториев поступают из control plane в формате HTTPS с именем хоста вашего git-хоста; для GitHub Enterprise это имя хоста, которое вы настроили для интеграции GitHub Enterprise на claude.ai. Два повторяемых флага перезаписывают эти URL перед клонированием:

  • --git-host-rewrite <from>=<to>: для split-horizon DNS, когда Anthropic обращается к вашему git-хосту по внешнему имени хоста, а раннеры должны использовать внутреннее
  • --git-ssh-rewrite <host>: для git-хостов, которые принимают только SSH, или для хоста раннера, у которого есть только ключ SSH. Например, чтобы клонировать репозитории github.com по SSH, добавьте --git-ssh-rewrite github.com в команду раннера. Тогда раннер перезаписывает https://github.com/owner/repo в git@github.com:owner/repo. SSH должен подключаться без интерактивного запроса.

Перезапись хоста выполняется первой, поэтому, если вам нужны обе, указывайте в --git-ssh-rewrite внутреннее имя хоста. Для полного контроля над извлечением используйте хук жизненного цикла checkout.

Запуск сессии для репозитория вне GitHub

Чтобы запустить сессию для репозитория с git-хоста, отличного от GitHub, запустите сессию из терминала в извлечённой копии этого репозитория. Сначала проверьте учётные данные git раннера, сеть и git-прокси, адрес репозитория и вашу ветку:

  • Учётные данные git и сеть: предоставьте учётные данные, которые работают без интерактивного запроса, для https://<host>/<owner>/<name> — адреса, который клонирует раннер, — и разрешите раннеру доступ к вашему git-хосту, как указано в разделе Сетевые требования. Для git-хоста, который принимает только SSH, см. --git-ssh-rewrite.
  • Git-прокси: если раннеры окружения используют git-прокси Anthropic, отключите его.
  • Адрес репозитория: проверьте удалённый репозиторий origin в извлечённой копии. Его хост должен быть доменным именем, например git.example.com, а путь должен иметь вид <owner>/<name>. Вложенный путь, например group/subgroup/name, не подходит.
  • Ветка: отправьте ветку, на которой вы находитесь, на ваш git-хост, поскольку раннер клонирует именно эту ветку.

В извлечённой копии, войдя в claude.ai, как описано в разделе Аутентификация из CI, выполните claude -p с --environment и ID вашего окружения ccpool_...:

claude -p "Fix the login bug" --environment ccpool_abc123 --output-format json

Когда сессия создана, команда выводит одну строку JSON, начинающуюся с {"ok":true. Если удалённый репозиторий origin не подходит, команда завершается ошибкой requires a git source, but no GitHub remote was detected.

Проверяйте и выполняйте слияние на вашем git-хосте веток, которые отправляют сессии.

Постройте образ runner

Anthropic не публикует предварительно построенный образ runner. Постройте свой собственный вокруг бинарного файла claude, наслаивая любой набор инструментов, который нужен вашим репозиториям: языковые среды выполнения, компиляторы, менеджеры пакетов и MCP sidecars.

Рецепты ниже используют --capacity 4, поэтому один контейнер обслуживает до четырех одновременных сеансов от одного заблокированного владельца. Это не обеспечивает изоляцию контейнера для каждого сеанса в разделе усиления безопасности: перед подключением окружения к production-системам либо запустите рецепты на --capacity 1 с одним контейнером на сеанс, либо используйте on-demand runners, которые также держат секрет окружения вне хостов, запускающих сеансы. Если вы добавите Anthropic git proxy к одному из этих рецептов, также измените --capacity на 1.

Этот Dockerfile является минимальной отправной точкой. Он устанавливает python3, потому что хуки, которые control plane Anthropic предоставляет сессиям Claude Tag, включая тот, с помощью которого Claude публикует свой ответ в Slack, запускают python3. Сохраняйте python3 в PATH в любом образе, который обслуживает сессии Claude Tag:

FROM debian:bookworm-slim
ARG CLAUDE_CODE_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates openssh-client jq python3 \
 && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "https://downloads.claude.ai/claude-code-releases/${CLAUDE_CODE_VERSION:?set with --build-arg CLAUDE_CODE_VERSION}/linux-x64/claude" \
      -o /usr/local/bin/claude && chmod +x /usr/local/bin/claude
RUN git config --system user.name "Claude" \
 && git config --system user.email "noreply@anthropic.com" \
 && git config --system --add safe.directory '*'
ENTRYPOINT ["claude"]

Замените linux-x64 на linux-arm64, если ваши узлы ARM, или на linux-x64-musl или linux-arm64-musl на образе на основе musl, таком как Alpine; см. Alpine Linux setup для дополнительных пакетов, которые нужны образам musl. URL является стандартным местоположением выпуска Claude Code, поэтому вы можете проверить загруженный бинарный файл по подписанному манифесту выпуска, как описано в Binary integrity and code signing. Runner требует версию Claude Code 2.1.224 или позже, либо 2.1.229 или позже для обслуживания сессий Claude Tag. Соберите образ, затем отправьте его в ваш реестр и ссылайтесь на него в рецептах ниже:

docker build \
  --build-arg CLAUDE_CODE_VERSION="$(curl -fsSL https://downloads.claude.ai/claude-code-releases/stable)" \
  -t <your-registry>/claude-runner:latest .

Подстановка команды ищет текущий номер выпуска stable и передает его как аргумент сборки, поэтому запуск той же команды после нового стабильного выпуска перестраивает слой загрузки с более новым бинарным файлом. Чтобы закрепить конкретный выпуск для воспроизводимых сборок, передайте номер версии непосредственно как CLAUDE_CODE_VERSION. Замените stable на latest в URL поиска, когда вам нужен выпуск новее, чем стабильный канал, например, тот, который требует недавно запущенная модель.

Размер CPU и памяти для сеансов

Размер контейнера или хоста runner для сеансов, которые он запускает, а не для самого процесса runner. Runner сам опрашивает работу, подготавливает checkout каждого сеанса, запускает ваши lifecycle hooks и запускает и контролирует процессы сеанса. Нагрузка исходит от сеансов: каждый из них является процессом Claude Code плюс все, что он запускает, такое как сборки, наборы тестов, установки пакетов и MCP servers.

Для одного сеанса начните со следующих значений, указанных как запросы и лимиты Kubernetes или эквивалент вашей платформы, и рассматривайте их как отправную точку, а не требование:

  • Память: запрос и лимит 4 ГиБ каждый, что соответствует минимуму 4 ГБ в системных требованиях Claude Code. Держите их равными, чтобы планировщик учитывал полную память контейнера. Когда контейнер достигает своего лимита памяти, ядро убивает процессы внутри него, что может завершить сеанс в середине задачи.
  • CPU: запрос 2 CPU и лимит 4 CPU, чтобы сеанс мог всплеснуть выше запроса во время сборок. Ядро дросселирует контейнер на его лимите CPU, а не убивает процессы в нем, поэтому сеансы на лимите работают медленнее, но продолжают работать.

В спецификации контейнера Kubernetes установите эти начальные значения с помощью следующего блока resources:

resources:
  requests:
    cpu: "2"
    memory: 4Gi
  limits:
    cpu: "4"
    memory: 4Gi

Сборки и тесты обычно являются самой большой и наиболее переменной частью нагрузки сеанса, поэтому запустите репрезентативную сборку вашего репозитория, измерьте его пиковый CPU и память и поднимите любое начальное значение, которое не оставляет места для процесса Claude Code на вершине этого пика.

Runner использует --capacity, чтобы ограничить число одновременно запускаемых сессий. Он не распределяет CPU или память между ними, поэтому сессии на одном runner делят CPU и память контейнера. Чтобы потребление памяти одной сессией не приводило к завершению остальных, запускайте одну сессию на runner с собственным лимитом памяти.

  • Один сеанс на runner: дайте каждому контейнеру значения одного сеанса. Используйте эту размер на --capacity 1, которую раздел усиления безопасности рекомендует, и для on-demand runners, где вы устанавливаете значения на рабочую нагрузку, которую ваш spawn-runner hook отправляет, такую как шаблон pod Kubernetes Job.

  • Несколько сеансов на runner: на --capacity выше одного, умножьте значения одного сеанса на емкость, потому что до такого количества сеансов может работать в контейнере одновременно. Рецепты Kubernetes и Docker Compose запускают --capacity 4 без лимитов CPU или памяти, поэтому добавьте лимиты, размер которых соответствует емкости, которую вы запускаете.

  • Несколько контейнеров runner на одном хосте: рассчитывайте размер хоста на сумму capacity всех runner, потому что каждый runner учитывает в --capacity только собственные сессии:

    • Память: умножьте память одной сессии на эту сумму, затем добавьте то, что потребляют собственные процессы хоста
    • CPU: умножьте request CPU одной сессии на эту сумму. Чтобы все сессии могли одновременно превышать request, умножайте вместо этого её limit CPU.

    Выделите каждому runner собственный контейнер. Используйте в каждом один и тот же путь --base-dir на хранилище, которое не монтирует контейнер никакого другого runner, чтобы у каждого runner был собственный рабочий каталог.

Задайте каждой сессии собственный лимит памяти

Чтобы при нехватке памяти одна сессия не приводила к завершению остальных, запускайте каждый runner с --capacity 1 под собственным лимитом памяти, равным памяти одной сессии:

  • В контейнере: запускайте один runner на контейнер и задайте лимит памяти контейнера, например limits.memory в Kubernetes, --memory в Docker или mem_limit в файле Compose. Следите, чтобы сумма лимитов контейнеров была меньше памяти хоста, потому что когда память заканчивается на самом хосте, ядро может завершать процессы в любом месте хоста.
  • Под systemd без контейнера: задайте MemoryMax для unit-а runner и запускайте один runner на хост, потому что runner, использующие один и тот же --base-dir в одной файловой системе, совместно используют клонированные туда репозитории. Например, при --capacity 1 и без хука checkout их сессии в одном репозитории могут одновременно работать в одном и том же checkout. Чтобы запустить несколько runner на одном хосте, выделите каждому контейнер.

Когда контейнер или unit достигает лимита памяти, ядро завершает процессы только внутри него. Если задать --capacity больше единицы, сессии делят один лимит. Когда они его достигают, ядро выбирает, какие процессы завершить, без учёта сессий, поэтому потребление памяти одной сессией может завершить остальные.

В Kubernetes разделить сессии не удастся ни при одном значении настройки kubelet singleProcessOOMKill. Ограничить общую память сессии с помощью ulimit для памяти в вашем скрипте-обёртке тоже не получится. Ядро применяет значение вроде ulimit -v к каждому процессу отдельно.

Оставьте запас под лимитами процессов и потоков

Чтобы подобрать лимит процессов и потоков, измерьте, сколько нужно одной сессии, и умножьте это на число сессий, которые делят лимит. Когда эти сессии достигают лимита, ядро отказывает в создании новых процессов и потоков, поэтому команды, сборки и тесты в любой из них могут завершаться ошибкой. Отклонённый вызов возвращает EAGAIN, стандартное сообщение которого — Resource temporarily unavailable.

Допустим, одна сессия достигает пика в 300 процессов и потоков, и четыре сессии делят лимит. Вместе им нужно 1 200, а при лимите 2 400 вы оставляете запас для сборок, которым нужно вдвое больше.

Где задать каждый лимит

Каждый лимит в этой таблице учитывает потоки наравне с процессами, а pids.max — это файл, в котором хранится лимит cgroup:

Лимит Где его задать Кто его делит
pids.max в Kubernetes podPidsLimit в конфигурации kubelet узлов, на которых работают runner. Задать его в спецификации Pod нельзя. Контейнеры одного Pod
pids.max в Docker --pids-limit или pids_limit в файле Compose Один контейнер
pids.max под systemd TasksMax для unit-а runner Один unit
ulimit -u Оболочка или служба, запускающая runner Всё на хосте, что работает под ID пользователя runner. Не применяется для root.

Если вы не задали TasksMax, systemd применяет DefaultTasksMax, который по умолчанию равен 15% от наименьшего из значений kernel.pid_max, kernel.threads-max и pids.max корневой cgroup.

Измерьте пик одной сессии

Чтобы получить значения для ваших runner, измерьте одну сессию на её хосте, которым в Kubernetes является узел:

1. Запустите один runner отдельно

Запустите один runner с --capacity 1, ничего больше не размещая в его контейнере или unit-е systemd, так чтобы он был один на своём хосте и в своём окружении:

  • Его хост: используйте хост, на котором не работает ни один другой runner. Тогда на следующем шаге вы сможете найти его процесс.
  • Его окружение: сделайте его единственным runner в окружении, например в окружении, которое вы создадите для теста. Сессии направляются в окружение, а не на конкретный runner.

Пока не запускайте сессию, потому что каталог из следующего шага нужен вам до начала сборки.

2. Найдите каталог cgroup runner

На хосте, а не внутри контейнера, найдите ID процесса runner, например с помощью ps. Прочитайте /proc/<pid>/cgroup для этого ID:

  • Есть строка, содержащая :pids:: хост использует cgroup v1, а каталог — это /sys/fs/cgroup/pids, за которым следует текст после :pids:
  • Единственная строка начинается с 0::: хост использует cgroup v2, а каталог — это /sys/fs/cgroup, за которым следует текст после 0::

3. Выполните репрезентативную сборку и запишите пик

Проведите одну сессию через репрезентативную сборку и возьмите пиковое значение из каталога cgroup runner:

  • В каталоге есть pids.peak: прочитайте этот файл после завершения сборки
  • В каталоге нет pids.peak: читайте pids.current раз в секунду во время сборки и сохраняйте наибольшее значение. Пик между двумя чтениями останется незамеченным, поэтому считайте это значение заниженной оценкой.

4. Умножьте на число сессий, делящих лимит

  • Pod, контейнер или unit одного runner: умножьте пик на --capacity этого runner
  • Лимит, который делят несколько runner: умножьте пик на сумму их capacity

Затем добавьте пик всего остального, что делит лимит, например другого контейнера в Pod.

5. Задайте лимит выше полученного результата

Более высокий лимит оставляет запас для более тяжёлых сборок, а более низкий быстрее останавливает fork-бомбу.

Проверьте лимит pids.max

  • Проверьте, был ли достигнут pids.max: найдите в логе ядра хоста, например с помощью dmesg, строку, содержащую cgroup: fork rejected by pids controller in и путь cgroup. Ядро записывает её в лог только в первый раз, когда отказывает в создании процесса или потока в этой cgroup, поэтому ищите по всему логу.
  • Узнайте действующий лимит: прочитайте pids.max в каталоге cgroup runner и в каждом каталоге выше него. Наименьшее значение и есть действующий лимит, а max означает отсутствие лимита на этом уровне.

Kubernetes

Runner обслуживает GET /healthz на порту 8080 по умолчанию, конфигурируемом с помощью --health-port, поэтому зонды Kubernetes работают без дополнительной настройки. Конечная точка возвращает 200 всякий раз, когда процесс живой, поэтому зонды ниже обнаруживают мертвый процесс, а не застрявший; чтобы поймать runner, который перестал опрашивать, установите оповещение на серию last_poll_age_seconds из /metrics. Развертывание ниже монтирует секрет окружения из Kubernetes Secret, указывает зонды liveness и readiness на /healthz и устанавливает период завершения 90 секунд. См. Shutdown timing для того, почему период завершения имеет значение.

Манифест не устанавливает resources CPU или памяти на контейнер runner. Добавьте блок, размер которого соответствует емкости, которую вы запускаете, как описано в Size CPU and memory for sessions.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: claude-runner
  namespace: claude-runners
spec:
  replicas: 3
  selector:
    matchLabels:
      app: claude-runner
  template:
    metadata:
      labels:
        app: claude-runner
        app.kubernetes.io/part-of: claude-code-self-hosted-runner
    spec:
      terminationGracePeriodSeconds: 90
      containers:
        - name: runner
          image: <your-registry>/claude-runner:latest
          args:
            - self-hosted-runner
            - --environment-secret-file
            - /etc/claude/environment-secret
            - --capacity
            - "4"
            - --base-dir
            - /workspace
          volumeMounts:
            - name: environment-secret
              mountPath: /etc/claude
              readOnly: true
          ports:
            - name: health
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 30
      volumes:
        - name: environment-secret
          secret:
            secretName: claude-runner-environment-secret

Развертывание выше живет в пространстве имен claude-runners. Сначала создайте пространство имен:

kubectl create namespace claude-runners

Создайте поддерживающий Secret из локального файла, содержащего значение, которое вы скопировали на шаге Copy environment key в пользовательском интерфейсе администратора, чтобы секрет никогда не появлялся в истории вашей оболочки. Запустите (umask 077 && cat > ./environment-secret), вставьте секрет, нажмите Enter, затем Ctrl-D. Затем создайте Secret и удалите файл:

kubectl create secret generic claude-runner-environment-secret -n claude-runners --from-file=environment-secret=./environment-secret

Docker Compose

Сервис Compose ниже перезапускает runner всякий раз, когда он выходит, что охватывает как сбои, так и нормальный выход после осушения. Политика перезагрузки Docker перезапускает тот же контейнер с его записываемым слоем нетронутым, поэтому runner возвращается на переиспользованной файловой системе, а не на свежей, которую рекомендует позиция усиления безопасности; используйте этот рецепт для оценки, и для production либо пересоздайте контейнер за запуск, либо используйте оркестратор, который это делает.

Docker ждёт дольше перед каждым перезапуском контейнера, который продолжает выходить, вплоть до потолка, поэтому runner, который не может запуститься, не продолжает перезапускаться в плотном цикле под этим рецептом. Когда runner выходит описывает, что проверить, когда это происходит.

services:
  claude-runner:
    image: <your-registry>/claude-runner:latest
    command:
      - self-hosted-runner
      - --environment-secret-file
      - /run/secrets/environment-secret
      - --capacity
      - "4"
      - --base-dir
      - /workspace
    secrets:
      - environment-secret
    restart: always
    stop_grace_period: 90s

secrets:
  environment-secret:
    file: ./environment-secret

Время завершения работы

[!NOTE]

Эти значения времени относятся к runner. У процесса claude self-hosted-runner orchestrator есть свои: см. Подберите --shutdown-drain-sec и таймаут остановки оркестратора.

При запуске каждый runner записывает в лог, сколько времени ему может понадобиться после SIGTERM, чтобы корректно завершить свои сессии, например This runner needs up to 80s при настройках по умолчанию. Таймаут остановки — это время, которое ваш супервизор процессов ждёт после SIGTERM, прежде чем убить runner. Установите его не меньше времени, записанного в лог:

  • Kubernetes: terminationGracePeriodSeconds. В Kubernetes значение по умолчанию составляет 30 секунд, поэтому без этой настройки он может остановить pod до того, как runner завершит работу.
  • Docker Compose: stop_grace_period

При получении SIGTERM runner перестаёт принимать новые сессии. Затем он начинает корректное завершение работы, называемое drain, если только вы не установили --defer-shutdown-max-min, чтобы отложить его. Drain состоит из трёх шагов:

  1. Runner ждёт до --drain-wait-sec секунд, по умолчанию 0, пока завершатся ещё выполняемые ходы.
  2. Он завершает дерево процессов каждой сессии, включая любую команду, которую Claude всё ещё выполнял, но не процесс, продолжающий работать после завершения своей shell-команды.
  3. Он запускает lifecycle-хук post-session.

Runner продолжает опрашивать Anthropic на протяжении всего drain. Благодаря этому его сессии остаются закреплены за ним, и другой runner не подхватит ни одну из них, пока ваш хук post-session ещё сохраняет незафиксированную работу.

Время, записываемое в лог, является суммой следующих значений:

При настройках по умолчанию это составляет 0 + 5 + 60 + 15 = 80 секунд. Более высокое значение --capacity не увеличивает это время, потому что runner выполняет drain всех своих сессий одновременно.

Выделите больше времени, если вы устанавливаете любой из этих флагов:

  • С --retire-at: оставьте между временем retire и временем остановки хоста достаточно времени для завершения типичных ходов, плюс ожидание фоновых задач, которое описывает Runner lifecycle, плюс время, записанное в лог. Вычисляйте время retire при каждом запуске, например date +%s плюс предполагаемое время жизни runner.
  • С --defer-shutdown-max-min: таймаут остановки также должен охватывать настроенные вами минуты и дополнительное ожидание перед началом drain, 75 секунд при настройках по умолчанию. Оба значения объясняются в разделе Defer the drain past the first signal. Runner также записывает это более длительное время в лог при запуске.

Дайте выполняемым ходам завершиться перед перезапуском

Чтобы дать выполняемым ходам завершиться перед rolling restart, установите --drain-wait-sec и увеличьте таймаут остановки как минимум до этого ожидания плюс 80 секунд. При значении ожидания по умолчанию 0 перезапуск прерывает любой ещё выполняемый ход, и сессия возобновляется на другом runner без своей неотправленной работы.

  • --drain-wait-sec: примерно столько, сколько длятся ходы и фоновые задачи, которым вы хотите дать завершиться. Это максимум: ожидание заканчивается раньше, если они завершатся первыми.
  • Ваш таймаут остановки: terminationGracePeriodSeconds в Kubernetes, stop_grace_period в Docker Compose. Например, при --drain-wait-sec 120 замените 90 в рецепте для Kubernetes или Docker Compose на 200.
  • Если вы изменили другие настройки завершения работы: 80 секунд рассчитаны на настройки по умолчанию. Запустите один runner с вашими флагами и используйте как минимум число из его стартовой строки This runner needs up to, которое включает ожидание. Если установлен --defer-shutdown-max-min, используйте большее число из следующей стартовой строки, после Size the stop timeout to at least. В разделе Время завершения работы перечислено, что увеличивает эти числа.

[!WARNING]

Если таймаут остановки истекает во время ожидания, ваш хост убивает runner, пока работа ещё выполняется. Его сессии не получают хук post-session. Runner также не отменяет регистрацию, поэтому control plane может потребоваться несколько минут, а иногда и больше, чтобы переставить их в очередь.

Опубликуйте статусное сообщение перед перезапуском

Перед перезапуском, который прервёт сессии, попросите Owner опубликовать статусное сообщение в среде на странице администратора Cloud environments. На claude.ai/code участники видят это сообщение в своих сессиях в этой среде. Попросите Owner указать срок действия при публикации или удалить сообщение, когда runner снова заработают.

Defer the drain past the first signal

Установите --defer-shutdown-max-min <n>, если вы хотите, чтобы runner, который вы перезапускаете, продолжал обслуживать сессии, которые он удерживает, до n минут, вместо того чтобы выполнять их drain при первом сигнале. При первом SIGTERM или SIGINT runner прекращает принимать новую работу и продолжает обслуживать сессии, которые он удерживает. Он продолжает опрашивать, чтобы control plane не переставлял эти сессии. Требует Claude Code v2.1.238 или позже.

Что происходит с сессиями, которые runner удерживает после первого сигнала

На первых двух этапах, следующих за сигналом, runner выпускает сессии, и выпущенная сессия возобновляется на свежем runner, когда её пользователь отправляет следующее сообщение. Отсчитывая от первого сигнала, runner проходит через три этапа:

  • В течение первых n минут: runner обслуживает свои сессии нормально и продолжает применять --startup-timeout-min и --kill-session-after-min. Если вы также установили --release-idle-session-min, runner выпускает любую сессию, пользователь которой был неактивен столько времени, если только вы не удерживаете эту сессию. Без этого флага неактивные сессии остаются на runner.
  • Когда истекают n минут: runner выпускает каждую сессию, которую он всё ещё удерживает, неактивную или нет. Runner ждёт, пока завершится ход сессии, находящейся посреди хода, и ещё до 60 секунд для фоновых задач этого хода, прежде чем выпустить эту сессию.
  • Когда истекает post-release grace: runner выполняет drain любых сессий, которые он всё ещё удерживает, и control plane сразу же переставляет каждую такую сессию на другой runner. Post-release grace начинается, когда истекают n минут, и составляет 75 секунд при значениях по умолчанию. Если вы установили --drain-wait-sec выше 60 секунд, post-release grace вместо этого составляет --drain-wait-sec плюс 15 секунд.

На любом этапе runner выходит с кодом 0, как только он не удерживает никаких сессий. Второй сигнал сокращает этапы: runner немедленно выполняет drain, как он это делает при первом сигнале без --defer-shutdown-max-min. Как только drain находится в процессе, следующий сигнал принудительно завершает runner. Это верно независимо от того, начал ли drain второй сигнал или истечение post-release grace.

Установите таймаут остановки

Дайте таймауту остановки вашего хоста по крайней мере сумму трёх частей: n минут, которые вы настраиваете, post-release grace и drain, который описывает раздел Время завершения работы. При настройках по умолчанию post-release grace составляет 75 секунд, а drain занимает до 80 секунд, поэтому выделите n минут плюс 155 секунд. Runner выводит эту сумму при запуске всякий раз, когда установлен --defer-shutdown-max-min.

Если таймаут остановки истекает до того, как runner завершит работу, сессии, которые он всё ещё удерживает, не получают хук post-session. Runner не отменяет регистрацию, поэтому control plane может потребоваться несколько минут, а иногда и больше, чтобы переставить эти сессии. Если вы не можете дать таймауту остановки эту сумму, оставьте --defer-shutdown-max-min неустановленным, чтобы runner вместо этого выполнял drain при первом сигнале.

Что достигает работающего хука post-session

Хук post-session и дочерний процесс сессии Claude каждый запускаются в своей собственной POSIX-группе процессов, отдельно от runner, поэтому механизмы остановки достигают их по-разному:

  • SIGTERM, пока runner уже выполняет drain: немедленно принудительно завершает runner, пропуская всё, что остаётся от drain. Без --defer-shutdown-max-min это второй SIGTERM, который получает runner. Ничто не отправляет сигнал работающему хуку post-session, поэтому на голом хосте, где init-процесс усыновляет осиротевшие процессы, он завершается самостоятельно, но без надзора: его бюджет таймаута больше не применяется, и запись в закрытый канал логов может убить его с SIGPIPE, поэтому хук, который должен пережить принудительное завершение в такой среде, должен перенаправлять свой собственный вывод в файл. В рецептах контейнеров на этой странице runner является PID 1 контейнера и его выход завершает контейнер, а при используемом systemd по умолчанию KillMode=control-group завершение всей cgroup достигает и хука, как описывает пункт Cgroup-wide kills; в обоих случаях рассматривайте принудительное завершение как фатальное для хука и полагайтесь вместо этого на период grace.
  • Process-group-wide signals, такие как kill -- -<pid> в скрипте-обёртке, управление заданиями оболочки или watchdog на уровне группы: достигают runner и подпроцесса, работающего посреди хука checkout, который намеренно остаётся привязанным к группе, но не работающего хука post-session или дочернего процесса сессии.
  • Cgroup-wide kills, такие как используемый systemd по умолчанию KillMode=control-group или SIGKILL, который Kubernetes доставляет всему контейнеру, когда истекает terminationGracePeriodSeconds: достигают всего, включая хук. Изоляция группы процессов не защищает от этого, поэтому период grace должен охватывать весь drain.
  • Собственный таймаут хука: когда хук превышает --post-session-hook-timeout-sec, runner отправляет SIGTERM на всю группу процессов хука, затем SIGKILL две секунды спустя, поэтому рабочий процесс, порождённый хуком, такой как tar, rsync или git, завершается вместе с оболочкой-обёрткой, а не выживает как осиротевший процесс. Надзор runner заканчивается, как только stdio хука закрывается: рабочий процесс, который перенаправил свой собственный вывод в файл и пережил этап SIGTERM, находится вне досягаемости runner.

Когда начинается drain, и снова при принудительном завершении, runner записывает в лог, сколько хуков post-session всё ещё работают, поэтому вы можете отличить тихий drain от того, который находится посреди создания снимка.

Держите базовый каталог и емкость идентичными на всех runners

Если runner умирает в середине сеанса, сервер переставляет сеанс в очередь и другой runner в окружении его подхватывает. Этот runner выводит путь checkout из своего собственного --base-dir и --capacity: --capacity 1 проверяет прямо под --base-dir, и --capacity выше 1 использует вместо этого per-session worktrees. Когда runners в одном окружении используют разные значения для любого флага, рабочий каталог возобновленного сеанса изменяется, и абсолютные пути, которые агент записал ранее, в редактированиях, вызовах инструментов или своих собственных заметках, указывают на местоположение, которое больше не существует.

Используйте одинаковые --base-dir и --capacity на каждом runner в окружении и не используйте значение для каждого хоста, такое как ID экземпляра или имя хоста. Передавайте оба флага явно, как это делают рецепты для Kubernetes и Docker Compose, чтобы ни одно из значений не зависело от значения по умолчанию.

Базовый каталог по умолчанию /workspace, с исключением, которое записывает строка справочника --base-dir. Runner нуждается в доступе на запись к нему. При запуске, перед регистрацией, runner создает каталог и подтверждает, что может писать в него, и выходит с cannot create or write to base directory, когда не может. Runner, запущенный как root, создает по умолчанию /workspace сам. Для non-root runner создайте каталог и дайте пользователю runner владение перед запуском runner, или укажите --base-dir на каталог, который этот пользователь уже владеет.

Переиспользуйте pre-warmed checkout

Для больших репозиториев клонирование может занимать бо́льшую часть времени запуска сессии. Чтобы пропустить холодное клонирование, подготовьте клон самостоятельно по тому пути, где runner хранит собственный. Без checkout хука runner хранит один канонический клон на репозиторий по пути <base-dir>/<repo-owner>/<repo> и переиспользует его между сессиями:

  • При --capacity 1: runner получает запрошенный ref, отсоединяет HEAD и выполняет жесткий сброс на него, что происходит почти мгновенно, когда изменилось немногое.
  • При --capacity больше единицы: runner выполняет fetch в этот клон, а затем создает из него отдельный worktree для каждой сессии. Pre-warmed клон экономит загрузку, но не checkout.

Подготовьте клон в образе или на постоянном томе:

  • Клон в образе: постройте клон в образ runner на этом пути. Каждый свежий контейнер затем начинается с теплым клоном без переиспользования диска.
  • Клон на постоянном томе: на runners, которые вы предварительно блокируете для учетной записи одного пользователя с --lock-to-account, укажите --base-dir на постоянный том, поэтому диск только когда-либо обслуживает эту учетную запись. Pre-locked runner никогда не подхватывает сеансы канала Claude Tag, поэтому этот вариант не применяется к runners, которые их обслуживают.

Что путь переиспользования делает и не гарантирует:

  • Глубина клона: создавайте клон без --depth, с --single-branch или без него. Runner никогда не передает --depth при выполнении fetch в клон с удаленным репозиторием origin, даже если вы задали CLAUDE_RUNNER_FETCH_DEPTH. С полным клоном каждая сессия загружает только те коммиты, которых нет в клоне. С неглубоким клоном сессия в ветке, ответвившейся до самого старого коммита клона, загружает всю историю этой ветки.

  • Отслеживаемые изменения сбрасываются, неотслеживаемые файлы сохраняются: при --capacity 1 каждая сессия начинается с жесткого сброса, который стирает отслеживаемые изменения предыдущей сессии, но runner никогда не запускает git clean, поэтому неотслеживаемые файлы из более ранних сессий заблокированного владельца остаются в дереве.

  • Каталоги для каждого сеанса также сохраняются: рядом с checkout runner создает записи для каждого сеанса под <base-dir>/_sessions/ для каждого сеанса, который он запускает. Локальный каталог конфигурации Claude сеанса содержит локальную копию транскрипта беседы. Рядом с ним находятся загруженные файлы сеанса, когда сеанс их имеет. Каталог сеанса находится там же: он содержит любые worktrees для каждого сеанса и checkout hook checkouts во время работы сеанса, и он сохраняет все остальное, что Claude написал в нем.

    По умолчанию runner оставляет их на месте, когда сеанс заканчивается, поэтому на диске, который пережил процесс runner, они накапливаются. Каждый сеанс запускается как собственный пользователь runner, поэтому любой более поздний сеанс, который использует этот диск, может их прочитать. Если вы сохраняете постоянный --base-dir, рассчитайте размер тома с учётом этого роста. То же самое применяется к любой установке, которая перезапускает runner на той же файловой системе, включая Docker Compose рецепт.

  • С --remove-session-state, каталоги для каждого сеанса не сохраняются: запустите runner с --remove-session-state, чтобы он удалял каталоги для каждого сеанса по мере завершения сеанса. Удаление является наилучшей попыткой: каталоги остаются, когда runner убивается до того, как его очистка запустится. Канонический клон и файлы, которые сеанс написал в другом месте на хосте, такие как временный каталог, остаются независимо.

  • С git proxy сброс превращается в checkout: с --use-anthropic-git-proxy runner очищает .git/ клона перед каждой сессией. Он сохраняет хранилище объектов и состояние shallow, а индекс удаляет. Без индекса каждая сессия начинается с полного checkout рабочего дерева вместо почти мгновенного сброса. Runner по-прежнему выполняет fetch в существующий клон, а не клонирует репозиторий заново. Pre-warm подмодулей при использовании прокси не поддерживается.

  • С git proxy pre-warm в формате reftable теряет свои refs: очищая .git/ перед каждой сессией, runner сохраняет .git/refs/ и удаляет каталог .git/reftable/. Клон, подготовленный в формате refs reftable в git, при этом теряет свои refs, поэтому выполняйте pre-warm клона в формате refs files.

  • Длинные клоны не нуждаются в обходном пути: runner ограничивает каждую операцию git с помощью 120-секундного наблюдателя без прогресса и 30-минутного жесткого лимита, а не плоского тайм-аута, поэтому медленный холодный клон, который продолжает сообщать о прогрессе, завершается.

Убедитесь, что runner использует ваш клон

Чтобы убедиться, что runner использует ваш клон, запустите его с --log-level debug и начните сессию в репозитории. Runner записывает в лог Prefetched repo found at и путь к клону. Если вместо этого он записывает No prefetched repo, fresh init at, значит, по пути, указанному в этой строке, он не нашел пригодного клона. Проверьте, что ваш клон находится по этому пути, принадлежит пользователю runner и имеет удаленный репозиторий с именем origin.

Если runner находит ваш клон, но сессии по-прежнему запускаются медленно, проверьте, не является ли клон неглубоким.

Закрепите версию

Дочерний процесс каждой сессии по умолчанию запускает собственный бинарный файл runner, и runner отключает auto-update внутри сессий, которые он порождает. При таком поведении по умолчанию каждая сессия запускает версию, которую вы установили на хосте или встроили в образ. Обновление на уровне хоста вступает в силу в следующий раз, когда runner запускается.

Выберите, какую версию запускают ваши сессии и когда она меняется:

  • Перед тем как закрепить версию: проверьте версии Claude Code, которые требуют модели, для каждой модели, которую используют ваши сессии. Если модель требует более новую версию, чем та, которую запускают ваши сессии, сервер отклоняет запросы для этой модели с Claude Code does not support this model.
  • Чтобы держать флот на одной версии: постройте образ с закрепленной версией или на голом хосте установите конкретную версию и отключите auto-updates
  • Чтобы обновить фиксированный флот: прочитайте записи журнала изменений между вашей версией и той, которую вы устанавливаете, затем установите более новую версию или пересоберите образ и перезапустите runners
  • Чтобы обновить runners по требованию: прочитайте записи журнала изменений между вашей версией и той, которую вы устанавливаете, затем измените образ, который запускает ваш хук spawn-runner. Каждый новый runner получает новую версию. Runner, который уже запущен, включая резервный runner, запущенный --min-idle, сохраняет свою версию до завершения работы. Не перезапускайте его, потому что его рабочий заказ одноразовый.
  • Если сессии запускают другой бинарный файл: если вы используете --exec-path или хук command, чтобы запускать бинарный файл Claude Code, отличный от собственного бинарного файла runner, сессии запускают версию этого бинарного файла. Обновляйте этот бинарный файл при обновлении runner.
  • Плагины: рынки плагинов тоже не auto-update; установите FORCE_AUTOUPDATE_PLUGINS=1 в окружении runner, чтобы позволить плагинам auto-update, пока бинарный файл остается закрепленным

Масштабируйте флот

Ваш оркестратор решает, когда добавлять или удалять runners. Из-за блокировки one-owner-per-runner, минимальное количество реплик - это количество пользователей и агентов Claude Tag, которых вы ожидаете быть активными одновременно; --capacity контролирует параллелизм в пределах сеансов одного владельца, а не на всех владельцах.

Доступны два подхода к масштабированию:

  • Фиксированный флот: запустите статический набор реплик runner и масштабируйте на Prometheus metrics, которые обслуживает каждый runner
  • On-demand runners: запустите подкоманду claude self-hosted-runner orchestrator, которая опрашивает Anthropic для сеансов, которые поставлены в очередь без доступного runner и вызывает ваш hook spawn-runner для загрузки одного на сеанс. См. On-demand runners.

Освобождение простаивающих сессий во фиксированном флоте

Если сессии остаются в очереди во фиксированном флоте, хотя все runners в сети, причиной могут быть простаивающие сессии. По умолчанию сессия сохраняет свой слот после завершения своего хода, а её runner остаётся заблокированным за владельцем этой сессии. Сессии других владельцев не могут использовать свободные слоты этого runner. Если облачная запланированная задача вашей команды запускает новую сессию при каждом выполнении, её простаивающие сессии могут занять все слоты.

Чтобы проверить это, посмотрите claude_code_self_hosted_runner_session_idle_seconds в Prometheus metrics каждого runner. У каждой сессии, которую удерживает runner, есть собственный ряд: 0, пока сессия занята, в противном случае — количество секунд с момента перехода в простой. Выберите допустимое время простоя: дольше пауз, которые люди делают посреди задачи, и достаточно короткое, чтобы каждое выполнение освобождалось до начала следующего выполнения запланированной задачи. Сравните ряды с этим временем.

Чтобы освобождать простаивающие сессии, запускайте каждый runner с --release-idle-session-min и этим временем в минутах, например --release-idle-session-min 30. Тогда runner освобождает любую сессию, которая простаивает так долго, независимо от того, запустила её запланированная задача или человек, если только вы не удерживаете её ради оставленных ею запущенных процессов.

Сохранение простаивающей сессии, пока работают её процессы

Чтобы сохранять простаивающую сессию до завершения процессов, которые она оставила на вашем хосте, например заданий nohup, запустите runner с --hold-idle-file и продолжайте обновлять (touch) этот файл с хоста. Требуется Claude Code v2.1.311 или новее.

Эти шаги предполагают, что runner работает на хосте не от имени root. Если ваш runner работает от имени root, его сессии могут сами обновлять файл, поэтому сначала запустите его от имени выделенного пользователя.

1. Выберите путь к файлу

Выберите локальный обычный файл, например /var/lib/claude-hold/hold-idle, в каталоге, в который сессии не могут записывать, включая родительские каталоги, чтобы сессия не могла записать в него. Runner и его сессии используют одного пользователя ОС, поэтому сначала создайте этот каталог, принадлежащий другому пользователю, например root, с режимом 755.

2. Запустите runner

Из вашей оболочки запустите runner с --hold-idle-file, --release-idle-session-min и --max-hold-min — ограничением на удержание, отсчитываемым с момента перехода сессии в простой. Без него ограничение составляет один день. Задайте его больше, чем --release-idle-session-min, иначе ничего не будет удерживаться. Замените <writable-dir>, как описано в руководстве по быстрому старту. В этом примере простаивающие сессии освобождаются через 30 минут, а удерживаемая сессия освобождается не позднее чем через 4 часа после перехода в простой:

claude self-hosted-runner \
  --environment-secret-file /etc/claude/environment-secret \
  --base-dir '<writable-dir>' \
  --release-idle-session-min 30 \
  --hold-idle-file /var/lib/claude-hold/hold-idle \
  --max-hold-min 240

3. Обновляйте файл, пока работают процессы

Пока работают процессы, выполняйте touch для файла с хоста от имени владельца каталога чаще, чем раз в три минуты. Первый touch создаёт файл. Например, добавьте эту строку в crontab пользователя root:

* * * * * pgrep -x nightly-build > /dev/null && touch /var/lib/claude-hold/hold-idle

Эта строка выполняется раз в минуту и обновляет файл, пока работает любой процесс с именем nightly-build, поэтому сессия, запустившая такой процесс, удерживается вплоть до ограничения. Замените проверку pgrep тем способом, которым ваш хост определяет, что оставили запущенным сессии.

Проверка должна перестать срабатывать, когда они завершатся, иначе каждая простаивающая сессия на этом runner будет удерживаться до ограничения.

4. Убедитесь, что удержание работает

Пока работают процессы, проверяйте, что время изменения файла остаётся не старше трёх минут. Когда время простоя сессии истечёт, найдите idle release held в логе runner. Если такой строки нет, посмотрите leftover-hold= в строке watchdog config, которую runner записывает в лог при запуске: там указаны файл и ограничение или причина, по которой ничего не удерживается.

Через три минуты после последнего touch удержание заканчивается. Ещё примерно через минуту в логе появляется idle release no longer held, и runner освобождает сессию.

Даже если файл продолжает изменяться, удержание может закончиться, например в таких случаях:

  • Превышено ограничение: истекает --max-hold-min. Чтобы удерживать дольше, увеличьте его
  • Принудительное освобождение: вы задали --retire-at, --defer-shutdown-max-min или --kill-session-after-min, и этот предел наступил. Чтобы удерживать дольше, задайте более поздний предел

Известные проблемы и ограничения

Ниже приведены ограничения в этом выпуске с обходными путями, где они существуют.

Трафик коннектора покидает вашу сеть

Anthropic вызывает инструменты коннектора из своей собственной инфраструктуры, а не из вашего runner. Инструменты коннектора - это коннекторы claude.ai, такие как GitHub, Slack и Linear. Когда Claude использует коннектор в самостоятельно размещаемом сеансе, этот трафик идет через api.anthropic.com, а не исходит из границы вашей сети.

Чтобы держать коннектор вне самостоятельно размещаемых сеансов, отфильтруйте его с помощью allowedMcpServers и deniedMcpServers параметров политики. Claude Code применяет эти параметры к коннекторам, которые Anthropic доставляет, а также к серверам, которые вы сеете с хоста runner и серверам, которые пользователи добавляют, поэтому если вы развертываете список разрешений для других серверов, Claude Code блокирует доставленные коннекторы тоже. Чтобы держать коннекторы доступными наряду со списком разрешений на основе URL, добавьте записи, которые соответствуют путям прокси Anthropic для доставленных коннекторов:

  • https://api.anthropic.com/v2/ccr-sessions/*
  • https://api.anthropic.com/v1/code/sessions/*
  • https://api.anthropic.com/v1/code/mcp/*

Если трафик инструмента должен остаться внутри вашей сети, запустите эквивалентные инструменты как локальные MCP-серверы на образе runner вместо этого. См. MCP servers.

Некоторые сеансы не считаются неактивными

Сессия, удерживающая фоновую задачу, которая продолжает выполняться, не считается неактивной, поэтому --release-idle-session-min не освободит слот этой сессии. Сессия, ожидающая подтверждения, запрошенного изнутри выполняющегося вызова инструмента, также не считается неактивной. Задайте вместе с ним --kill-session-after-min в качестве жёсткой страховки.

--kill-session-after-min является упором для убегающих сеансов. На runner на v2.1.260 или позже, сеанс, который достигает лимита, не завершается сразу. Runner дает ему окно благодати, 15 минут по умолчанию, которое вы можете изменить с помощью SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS:

  • Если сеанс ждет своего пользователя, runner выпускает его. Если его ход закончился и он держит только фоновые задачи, runner ждет до 60 секунд, чтобы эти задачи завершились, а затем выпускает его. Сеанс возобновляется, когда его пользователь отправляет свое следующее сообщение.
  • Если ход все еще работает, runner ждет, пока ход закончится, или пока сеанс не будет ждать своего пользователя, и затем выпускает его.
  • Если сеанс все еще на runner, когда окно благодати истекает, runner завершает его, и любая работа работающего хода потеряна. Ход, ожидающий одобрения, запрошенного изнутри работающего вызова инструмента, - это один способ, которым сеанс переживает окно.

Освобождённая сессия возобновляется из свежего клона, поэтому работа, которую она не отправила, теряется в любом случае; см. Возобновлённые сессии теряют неотправленную работу. До v2.1.260 runner завершал каждую сессию по достижении лимита, ожидая завершения выполняющегося хода не дольше льготного периода.

Установите флаг выше вашего самого длинного ожидаемого сеанса, такой как --kill-session-after-min 480 для 8 часов. Чтобы освободить слоты из разговоров, которые становятся неактивными, используйте вместо этого --release-idle-session-min.

Возобновлённые сессии теряют неотправленную работу

Сессия, которая возобновляется на новом runner, теряет работу, которую она не отправила, поскольку этот runner заново клонирует репозиторий с её начальной ветки.

Чтобы сохранить закоммиченную работу, задайте --push-outcome-on-release на каждом runner в окружении. Runner с этим флагом перед освобождением по возможности отправляет (push) ветки результатов сессии, и возобновлённая сессия начинается с этих коммитов. Runner без него возобновляет сессию с её начальной ветки. Для push используются собственные учётные данные git хоста runner, в том числе на runner, который использует управляемый Anthropic git.

[!WARNING]

Прежде чем задать флаг, ограничьте круг тех, кто может выполнять push в refs claude/* на исходном удалённом репозитории. При возобновлении runner получает ранее отправленную ветку, не проверяя, кто её отправил.

Даже при заданном флаге сохраняйте эту работу другим способом:

  • Незакоммиченные изменения: они по-прежнему теряются. См. Побуждение сессий отправлять свою работу.
  • Репозитории, извлечённые с помощью хука жизненного цикла checkout: они не отправляются. Сохраняйте их снимки из хука post-session.
  • Коммиты в сессии, которую кто-то архивирует или удаляет либо чей runner освобождает из меню сессии: не рассчитывайте на push через флаг. Чтобы сохранить их, отправляйте HEAD в резервную ветку из хука post-session. Приведённый там пример выполняет push только после коммита-снимка, поэтому сделайте так, чтобы ваш хук выполнял push, даже когда незакоммиченных изменений нет.

Дополнительные ограничения

  • Команда, прерванная из-за потери runner, может выполниться повторно: когда сессия возобновляется на другом runner, Claude видит прерванный вызов инструмента с результатом, сообщающим, что его исход неизвестен.
  • Репозиторий, добавленный в середине сессии, может не клонироваться: Claude клонирует его с помощью git clone по HTTPS. На runner без --use-anthropic-git-proxy клонирование завершается ошибкой аутентификации git, если ничто на хосте не может прочитать репозиторий. По возможности выбирайте все необходимые сессии репозитории при её создании.
    • При возобновлении сессии: хук checkout запускается и для добавленного репозитория, при этом CLAUDE_RUNNER_REPO_REF пуст, поэтому убедитесь, что ваш хук обрабатывает этот случай.
  • Некоторые коннекторы не появляются в самостоятельно размещаемых сеансах: коннектор, который вы еще не подключили в параметрах claude.ai, не указан в самостоятельно размещаемом сеансе, и сеанс не будет вас приглашать подключить его. Подключите его в параметрах сначала, затем запустите свежий сеанс. Добавление коннектора в уже работающий сеанс тоже не делает его инструменты доступными для Claude; запустите свежий сеанс, чтобы подхватить недавно добавленный коннектор.

Сообщите о проблеме

Для проблем с самостоятельно размещаемыми окружениями свяжитесь с командой вашего аккаунта Anthropic.

Устранение неполадок

Для пошаговой диагностики запустите подкоманду doctor на хосте runner'а. Подкоманда doctor запускает интерактивную сессию Claude Code с подключёнными логами и состоянием runner'а. Сначала войдите в систему с помощью claude auth login на этом хосте, чтобы сессия могла запрашивать данные о вашем окружении, его runner'ах и его сессиях в очереди. Без этого входа, например когда хост проходит аутентификацию с помощью API-ключа, она ограничена локальным эндпоинтом проверки работоспособности, метриками и логом runner'а, причём лог она читает, только если вы запустили runner с --log-file.

claude self-hosted-runner doctor

Частые проблемы:

  • Runner не появляется в окружении: убедитесь, что хост может подключиться к api.anthropic.com по HTTPS, а секрет окружения актуален. При сбое аутентификации runner записывает в лог [runner:fatal] с причиной отказа.

  • Runner завершается при запуске с cannot create or write to base directory: runner не может создать --base-dir или записать в него; по умолчанию это /workspace. Исправьте владельца каталога или укажите в --base-dir путь, доступный для записи, как описано в разделе Сохраняйте одинаковые базовый каталог и ёмкость на всех runner'ах. Если вместо этого runner записывает в лог [runner:fatal] с сообщением о том, что истёк таймаут проверки базового каталога, каталог находится на зависшем монтировании NFS или CSI. Проверьте работоспособность монтирования, а не права доступа. Runner выводит оба этих сбоя запуска в stderr до того, как откроет --log-file, поэтому ищите их в терминале или в логах контейнеров вашей платформы, а не в файле лога. До v2.1.225 runner не проверял базовый каталог при запуске, и такая ошибка конфигурации вместо этого приводила к сбою сессий после их принятия.

  • Runner завершается при запуске с RegisterRunner failed: и ошибкой TLS-сертификата: например, ошибка может быть self signed certificate in certificate chain. Runner не доверяет центру сертификации (CA), выпустившему полученный сертификат, как бывает за прокси с инспекцией TLS. Задайте в окружении runner'а переменную NODE_EXTRA_CA_CERTS с путём к PEM-файлу, содержащему корневой CA прокси.

    Для сессий её не нужно задавать повторно — они её наследуют. Git её не читает: для клонирования через этот прокси также задайте GIT_SSL_CAINFO.

  • Ваш стартовый скрипт выводит ошибку записи, содержащую Resource temporarily unavailable, и завершается: ваш скрипт делит stdout или stderr с запускаемым им процессом claude self-hosted-runner или оркестратора, который переключил этот общий канал или сокет в неблокирующий режим. Когда канал заполнен, запись из вашего скрипта вместо ожидания завершается этой ошибкой операционной системы, и при set -e скрипт завершается.

    Чтобы устранить ошибку, направьте вывод runner'а или оркестратора в файл, который может создать пользователь вашего скрипта. Добавьте >> "$HOME/claude-runner.log" 2>&1 к команде, которая его запускает, перед любым &, переводящим её в фоновый режим, и замените <writable-dir>, как описано в разделе быстрого старта:

    claude self-hosted-runner --environment-secret-file /etc/claude/environment-secret --base-dir '<writable-dir>' >> "$HOME/claude-runner.log" 2>&1
    

    Собственный вывод вашего скрипта по-прежнему идёт туда же, куда и раньше, а то, что runner пишет в stdout и stderr, включая ошибки запуска, попадает в этот файл. Используйте это перенаправление оболочки, а не только --log-file, который дублирует лог в файл, пока runner продолжает писать в stdout и stderr. Если вы уже передаёте --log-file, сохраните и его, поскольку перенаправление не меняет того, что попадает в этот файл.

  • Оркестратор записывает в лог poll failed: с ошибкой TLS-сертификата и продолжает повторные попытки: оркестратор не доверяет CA, выпустившему полученный сертификат, как бывает за прокси с инспекцией TLS. Задайте в окружении оркестратора переменную NODE_EXTRA_CA_CERTS с путём к PEM-файлу, содержащему корневой CA прокси. Задайте её также для runner'ов, которые запускает ваш хук spawn-runner.

    Например, строка может заканчиваться на poll failed: self signed certificate in certificate chain — retrying in 1s, где между двоеточием и тире стоят слова TLS-библиотеки. Чтобы убедиться, что проблема устранена, проверьте, что connected имеет значение true в теле ответа /healthz оркестратора.

  • Сессии остаются в очереди: возможно, каждый работающий runner закреплён за другим владельцем. Проверьте метрику claude_code_self_hosted_runner_locked_account каждого runner'а или поле locked_account его строки лога [runner:health], чтобы узнать, за кем он закреплён. Оба показывают email владельца только после того, как runner получил токен сессии с утверждением act.email, чего никогда не бывает с сессиями агента Claude Tag. Без этого утверждения runner не выдаёт серию locked_account и записывает в лог locked_account=yes, что говорит о том, что runner закреплён, но не за каким владельцем. Добавьте реплики или дождитесь, пока существующий runner завершит работу и перезапустится. Если окружение использует runner'ы по запросу, проверьте вместо этого оркестратор; см. Runner'ы по запросу.

  • Сессии завершаются сбоем сразу после принятия: откройте сессию в claude.ai/code, чтобы увидеть ошибку. Если ошибка показывает, что git не смог пройти аутентификацию, проверьте учётные данные git runner'а. Для runner'а, запущенного с --use-anthropic-git-proxy, см. Если сессии не запускаются на runner'е с git-прокси.

    Базовый каталог, недоступный для записи, останавливает runner при запуске, а не приводит к сбою сессий. См. пункт Runner завершается при запуске с cannot create or write to base directory в этом списке.

  • Сессия принята, но Claude так и не отвечает на её первое сообщение: если ваши runner'ы подключаются к api.anthropic.com через прокси, настройте его так, чтобы ответ /worker/events/stream сессии передавался по мере поступления. Увеличение таймаутов runner'а не поможет. Чтобы подтвердить причину по логу runner'а, см. Сессия принята, но Claude так и не отвечает на её первое сообщение.

  • Сессии не запускаются на runner'е, для которого задан --use-anthropic-git-proxy: ищите в логе runner'а access denied by the git proxy или ошибку git, в которой указан адрес api.anthropic.com, содержащий /git_proxy/. Чтобы определить, обслуживала ли Anthropic сессию, и устранить причину, см. Если сессии не запускаются на runner'е с git-прокси.

  • Сессии не могут выйти в сеть через исходящий прокси с аутентификацией: когда источник, заданный с помощью --proxy-authorization-command или --proxy-authorization-file, завершается сбоем, превышает таймаут в 30 секунд или возвращает пустое значение, runner отвечает на это соединение 502 Bad Gateway и записывает причину в лог. Runner скрывает stderr команды в этом логе и никогда не записывает в лог значение заголовка. При использовании --proxy-authorization-command выполните команду на хосте самостоятельно, чтобы убедиться, что она выводит полное значение заголовка в stdout. Если вместо этого runner завершается при запуске с could not start the proxy-authorization listener, он не смог открыть свой loopback-слушатель.

  • Runner записывает в лог строки Poll failed, содержащие rejecting the malformed poll response: runner получил ответ на опрос очереди работ, тело которого не является ожидаемым JSON очереди. Одна из причин — перехватывающий прокси или captive portal, отвечающий собственной страницей. Если runner подключается через прокси, настройте прокси так, чтобы ответы от api.anthropic.com передавались без изменений.

    Runner отклоняет некорректный ответ, учитывает его в категории transport метрики claude_code_self_hosted_runner_poll_errors_total и повторяет попытку по расписанию для неудачных опросов, описанному в разделе Жизненный цикл сессии. Runner продолжает обслуживать свои активные сессии. До v2.1.246 runner воспринимал такой ответ как пустую очередь работ, что могло завершить его активные сессии или привести к его завершению.

  • Сессия завершается сбоем с no longer exists on the remote или couldn't find remote ref: runner воспринимает сообщение git couldn't find remote ref как удалённую ветку. При этом ветка может по-прежнему существовать. Поищите её на вашем git-хосте точно под тем именем, что указано в сообщении, затем проверьте каждую подходящую причину:

    • Ветка удалена: восстановите её, как просит сообщение.
    • Ветка существует, и вы не администрируете runner: отправьте администратору runner'а полное сообщение и сообщите, что ветка существует.
    • Ветка существует, и у runner'а есть хук checkout: команда git в вашем хуке может вывести couldn't find remote ref в stderr до того, как хук завершится с ненулевым кодом. Найдите эту команду и исправьте ref, который она запрашивает, или remote, из которого она выполняет fetch. Runner передаёт вашему хуку ревизию, запрошенную сессией, в переменной CLAUDE_RUNNER_REPO_REF.
    • Ветка существует, и у runner'а нет хука checkout: runner сам получает ветку. Если к репозиторию применяется --git-host-rewrite, убедитесь, что на хосте, на который выполняется перезапись, есть ветка с тем же именем, и отправьте ветку туда, если её нет.
    • Ветка существует, и runner старше v2.1.301: обновите runner, как описано в разделе Закрепите версию, чтобы другой сбой показывал собственную ошибку. До v2.1.301 другие сбои, в выводе ошибки которых был couldn't find remote ref, также отображались как удалённая ветка.

    После обновления runner'а или устранения причины повторите сессию или запустите новую. Если ни одна из этих проверок не выявила причину, сообщите о проблеме.

    Для источника, в который сессия отправляет результаты, удалённая ветка приводит к сбою сессии. Для git-источника, из которого сессия только читает, runner пропускает этот источник и продолжает работу с остальными. Runner завершает сессию сбоем, если пропуск оставил бы её вообще без репозитория. До v2.1.228 такая сессия запускалась в пустом каталоге.

  • Claude Tag иногда не публикует свой ответ в Slack: напоминание об ответе — это хук, который предоставляет плоскость управления. Убедитесь с помощью claude --version, что runner работает на Claude Code v2.1.229 или новее, поскольку более старые runner'ы игнорируют предоставленные хуки; убедитесь, что python3 --version успешно выполняется внутри образа runner'а, поскольку хуки запускают python3; и убедитесь, что ни один файл настроек не отключает хуки — ни disableAllHooks в managed-settings.json, в ~/.claude/settings.json пользователя runner'а или в .claude/settings.json либо .claude/settings.local.json репозитория, ни allowManagedHooksOnly или strictPluginOnlyCustomization, охватывающий хуки, в управляемых настройках.

  • Сессия запускается без одного из своих репозиториев: на runner'е без хука checkout git-хост может отклонить проверку доступа runner'а к репозиторию, из которого сессия только читает. Тогда runner пропускает этот репозиторий, записывает в лог строку [runner:warn] could not access context source с указанием отказа и запускает сессию с остальными репозиториями.

    С собственными учётными данными git runner может пропустить репозиторий, если хост отвечает, что репозиторий не найден, git не находит учётных данных для хоста или аутентификация не проходит.

    С --use-anthropic-git-proxy, когда git-прокси отклоняет репозиторий с access denied by the git proxy, runner пропускает его. Тогда в строке лога указано blocked by the git proxy. Причины, которые следует проверить, см. в случае remote: access denied by the git proxy в разделе Если сессии не запускаются на runner'е с git-прокси.

    Сбой сети, таймаут или любой другой HTTP 403 по-прежнему приводят к сбою запуска сессии, как и отказ для репозитория, в который сессия отправляет результаты. Runner по-прежнему завершает сбоем сессию, которую пропуск оставил бы вообще без репозитория.

    Проверка доступа выполняется заново при каждом запуске сессии на runner'е, поэтому, как только git-идентичность runner'а получит доступ на чтение, следующий запуск клонирует репозиторий. До v2.1.274 каждый из этих отказов приводил к сбою запуска сессии.

  • В логе есть could not read context source: эта строка, в отличие от could not access context source, означает, что git-хост продолжал отвечать HTTP 429 или обёртка, которую вы установили в качестве git, продолжала завершаться с кодом 75. Runner пропустил этот репозиторий. Чтобы повторить попытку, запустите новую сессию. Runner также снова пробует этот репозиторий каждый раз, когда эта сессия запускается на runner'е.

    Без хука checkout runner может пропустить репозиторий, из которого сессия только читает, если только он не первый среди репозиториев сессии. До v2.1.311 запуск сессии завершался сбоем.

  • Сессия не запускается с This runner couldn't read: runner запросил у git-хоста репозиторий со своими учётными данными git и получил ответ «не найдено». Репозиторий не существует, или эти учётные данные не дают доступа к репозиторию.

    • Если вы администрируете runner: предоставьте его учётным данным git доступ на чтение к репозиторию, затем запустите новую сессию.
    • Если вы запустили сессию: если репозиторий не существует, запустите новую сессию с правильным репозиторием. Если у runner'а нет доступа к репозиторию, попросите администратора runner'а предоставить доступ, затем запустите новую сессию.
  • Сессии запускаются или возобновляются несколько минут: одна из причин — клонирование с нуля. В парке, где каждый runner запускается на пустом диске без хука checkout, runner заново клонирует репозитории сессии, прежде чем запустить Claude Code. Сессия, которая возобновляется на пустом диске, снова ждёт того же клонирования.

    Чтобы избежать клонирования с нуля, предоставьте предварительно подготовленный checkout. Если вы уже его предоставляете, убедитесь, что runner его использует. Если runner сам создаёт клон и завершается после одной сессии, уменьшите CLAUDE_RUNNER_FETCH_DEPTH со значения по умолчанию в 50 коммитов, чтобы получать меньше истории.

    Если вы используете хук, он выполняется вместо клонирования runner'ом, поэтому вместо этого проверьте, что делает ваш хук. Следите за метрикой claude_code_self_hosted_runner_session_init_duration_seconds, чтобы видеть, сколько времени занимают запуски.

  • Ходы завершаются с ошибкой 401: когда ход заканчивается ответом 401 или 403 от Anthropic API, runner получает новый CLAUDE_CODE_OAUTH_TOKEN от Anthropic и передаёт его сессии. Неудавшийся ход не повторяется. Этот токен краткосрочный, и runner ротирует его через stdin сессии.

    Когда получение токена не удаётся, runner записывает в лог строку inference_token refresh failed с указанием, когда будет повторная попытка, и продолжает повторять попытки, пока работает сессия.

    Если все вызовы начинают завершаться сбоем примерно через 30 минут после начала сессии, вероятно, скрипт-обёртка отключил stdin сессии, поэтому ротации токена не могут до неё дойти; см. Сохраняйте подключёнными stdin и файловый дескриптор 3.

    До v2.1.274 runner прекращал повторные попытки получения токена после нескольких неудач и ждал следующего запланированного получения. Неудавшийся ход не вызывал получение токена, поэтому каждый ход завершался ошибкой 401 до следующего запланированного получения.

  • Ход завершается в claude.ai/code с This model needs a newer version of Claude Code: версия Claude Code, с которой ваши runner'ы запускают сессии, старше, чем требует модель сессии, поэтому API отклоняет запрос. Обновите runner'ы до версии, указанной в сообщении, или более поздней, как описано в разделе Закрепите версию. До этого пользователи могут выбрать в сессии другую модель.

  • Под завершается принудительно во время drain: увеличьте terminationGracePeriodSeconds как минимум до значения, которое runner записывает в лог при запуске. См. Время завершения работы.

  • Runner записывает в лог строку [runner:log], заканчивающуюся на was paused or too slow: runner продолжил работу, и перезапуск не требуется. Пока поток stdout или stderr, названный в строке, снова не начали читать, runner отбрасывал все строки этого потока, кроме последних 256 КиБ. Если вы передали --log-file, ищите отброшенные строки там.

  • Runner записывает в лог Nobody can read this process's stdout anymore: то, что читало stdout runner'а, больше не существует, поэтому runner прекращает писать туда до конца своей работы и продолжает работать. Пока stderr остаётся открытым, эта строка и последующие строки состояния runner'а, включая строки [runner:fatal], идут в stderr, поэтому читайте их там.

После инициализации логирования runner пишет свой лог жизненного цикла, включая строки [runner:fatal], в stdout, а отладочный вывод — в stderr, всё в виде строк простого текста, а не JSON. Сбои базового каталога, описанные в пунктах выше, выводятся в stderr до этого момента. Сохраняйте оба потока с помощью --log-file, что также позволяет self-hosted-runner doctor отслеживать их, или с помощью средств сбора логов вашей платформы.

Дочерний процесс каждой сессии пишет отдельный отладочный лог. При сбое runner показывает конец этого лога рядом с сессией в claude.ai/code. Если вы не запустили runner с --remove-session-state, он также сохраняет лог неудавшейся сессии на диске и выводит его путь в лог runner'а.

Когда runner завершается

Не перезапускайте runner по запросу, поскольку его рабочее задание одноразовое. Runner, который завершается сразу после запуска, требует иного подхода, чем runner, завершившийся по любой другой причине.

  • Нормальное завершение: runner завершил свои сессии и выполнил drain, достиг времени вывода из эксплуатации или получил команду остановиться. Перезапустите его, чтобы у окружения снова была ёмкость. Эти завершения описаны в разделе Жизненный цикл runner'а.
  • Неудачный запуск: runner не может запуститься с заданной ему конфигурацией или на заданном хосте, поэтому он завершается через несколько секунд после запуска и завершается так же при каждом перезапуске. Более быстрый перезапуск не поможет. Кому-то нужно прочитать его вывод и устранить причину.
  • Потеря связи: runner, который не может связаться с Anthropic дольше срока своей аренды, например пока его хост находится в спящем режиме, может быть удалён из окружения. Когда удалённый runner снова подключается, он завершается. В его логе может быть строка [runner:fatal], содержащая runner record gone server-side или, после более длительного перерыва, poll auth failed. Runner не регистрируется повторно самостоятельно, поэтому перезапустите его.

Настройте супервизор так, чтобы он перезапускал runner при каждом его завершении, увеличивал паузу между перезапусками, когда runner продолжает завершаться сразу после запуска, и оповещал кого-либо, когда это продолжается.

В продакшене пусть супервизор перезапускает runner на чистом диске, в новом контейнере или VM, как рекомендует раздел Усильте защиту развёртывания.

Как распознать неудачный запуск

Когда runner не может запуститься, он выводит строку с причиной, а затем завершается. В большинстве случаев строка содержит [runner:fatal]. В некоторых случаях строка вместо этого начинается с error:, в том числе когда runner не может разобрать свои флаги, не может прочитать секрет окружения или не может создать базовый каталог или записать в него. Следующая строка тогда указывает на --help.

Большинство строк лога начинаются с временной метки и [self-hosted-runner], которые в примере ниже опущены. Например, runner, запущенный с git-прокси Anthropic и ёмкостью больше единицы, выводит строку вроде этой:

[runner:fatal] --use-anthropic-git-proxy requires --capacity 1 (the proxy URL is per-session and linked worktrees share origin). Omit --use-anthropic-git-proxy or set --capacity 1.

Ищите эту строку в стандартном выводе и стандартном потоке ошибок runner'а, в логах контейнеров вашей платформы или в файле, заданном с помощью --log-file. Runner выводит строку error: до того, как откроет файл лога, поэтому ищите её в терминале или в логах контейнера, как отмечено в разделе Устранение неполадок.

При разборе неудачного запуска также помогут следующие моменты:

  • Строки нет вовсе: runner, который принудительно завершил хост, не выводит ни той, ни другой строки. Если вывод заканчивается без строки [runner:fatal] и без строки error:, проверьте, не остановил ли процесс хост или ваш оркестратор, например из-за превышения лимита памяти.
  • Код выхода: runner не выделяет отдельный код выхода для ошибок, повторяющихся при каждом запуске. Он завершается с одним и тем же кодом как при ошибке конфигурации, например неподдерживаемом сочетании флагов, так и при сбое, который может устраниться сам, например когда API остаётся недоступным на протяжении собственных повторных попыток runner'а. Решение об увеличении паузы принимайте исходя из того, как скоро runner завершился, а причину выясняйте по выводу runner'а.
  • Окружение выглядит работоспособным: некоторые шаги запуска выполняются после регистрации runner'а в окружении, например --configure-git и настройка учётных данных git-прокси Anthropic. Если один из этих шагов завершается сбоем, окружение может ещё несколько минут после завершения процесса продолжать отображать этот runner, и страница Cloud environments может показывать Healthy, хотя ни один runner не принимает работу. Если сессии остаются в очереди в окружении, которое выглядит работоспособным, проверьте, не перезапускает ли ваш супервизор runner.

Перезапуск с растущей паузой

Способ получить растущую паузу зависит от вашего супервизора.

  • Kubernetes: Deployment на этой странице не требует изменений. После завершения контейнера kubelet по умолчанию ждёт перед его перезапуском, и пауза растёт с каждым перезапуском до верхнего предела. Пауза сбрасывается, как только контейнер некоторое время проработает без завершения.

    Kubelet применяет ту же паузу и после нормального завершения, если контейнер проработал недолго. Поэтому runner, который часто выполняет drain, тоже может показывать статус CrashLoopBackOff, так что прочитайте вывод, прежде чем заключать, что runner не может запуститься. Команда ниже читает вывод последнего запуска одного пода Deployment:

    kubectl logs --previous -n claude-runners deploy/claude-runner
    

    Если последний запуск был неудачным, строка [runner:fatal] или error: находится среди последних строк вывода. Чтобы прочитать последний запуск другого пода, укажите этот под вместо deploy/claude-runner.

  • Docker и Docker Compose: рецепт Compose на этой странице не требует изменений. С restart: always Docker ждёт всё дольше перед каждым перезапуском контейнера, который продолжает завершаться, до верхнего предела. Замените <container> на имя контейнера в команде ниже, которая показывает, сколько раз Docker перезапускал контейнер:

    docker inspect --format '{{.RestartCount}}' <container>
    

    Команда выводит число. Если число продолжает расти, Docker продолжает перезапускать runner.

  • Юнит systemd: по умолчанию systemd ждёт одинаковое время RestartSec перед каждым перезапуском и не увеличивает его, поэтому юнит с Restart=always перезапускает runner, который не может запуститься, каждый раз с одним и тем же интервалом. Когда запуски происходят достаточно часто, чтобы достичь ограничения частоты запусков юнита (по умолчанию пять запусков за 10 секунд), systemd прекращает перезапускать юнит. Юнит остаётся остановленным, пока кто-нибудь не запустит его снова, что systemd разрешает после истечения интервала ограничения частоты запусков или после systemctl reset-failed. Поскольку RestartSec применяется к каждому перезапуску, большее значение также задерживает перезапуск после нормального завершения. Выберите значение, которое уравновешивает эти два случая, и настройте оповещения по счётчику перезапусков юнита.

  • Цикл в оболочке или собственный супервизор: примените то же правило самостоятельно. Начните с паузы в пять секунд. После каждого запуска, который завершился в пределах минуты, удваивайте паузу перед следующим перезапуском, максимум до пяти минут. После запуска, который длился минуту или дольше, вернитесь к пяти секундам.

Выясните, почему runner продолжает завершаться

Если runner несколько раз подряд завершился сразу после запуска, остановитесь и проверьте следующее, прежде чем снова его перезапускать.

  • Последняя строка [runner:fatal] или error:: в ней сказано, почему runner остановился. Частые причины перечислены в разделе Устранение неполадок.
  • Сочетание флагов: git-прокси Anthropic требует --capacity 1. Рецепты на этой странице используют бо́льшую ёмкость, поэтому уменьшите её, когда добавляете прокси в один из них.
  • Что доступно окружению службы: если runner запускается вручную, но завершается сбоем под вашим супервизором, сравните пользователя, домашний каталог, PATH и лимит памяти. --configure-git и git-прокси Anthropic требуют наличия git в PATH и доступного для записи ~/.gitconfig.
  • Секрет окружения: если вы отозвали секрет или ошиблись при его вводе, runner выводит строку, содержащую RegisterRunner auth failed.
  • Вкладка Activity окружения: откройте окружение и выберите Activity. Если там продолжают появляться новые runner'ы и ни один из них не принимает работу, ваш супервизор перезапускает runner.

Для пошаговой диагностики на хосте runner'а запустите подкоманду doctor.

Сессия принята, но Claude так и не отвечает на её первое сообщение

Если runner принимает сессию, но Claude так и не отвечает на первое сообщение, а ваши runner'ы подключаются к api.anthropic.com через прокси, причиной может быть прокси. Anthropic отправляет сессии её сообщения через один потоковый ответ, который остаётся открытым. Если ваш прокси буферизует его или закрывает, прежде чем что-либо передать, первое сообщение так и не доходит. Другие запросы сессии при этом выполняются успешно.

Чтобы это исправить, измените то, как прокси обрабатывает этот запрос:

GET https://api.anthropic.com/v1/code/sessions/<session-id>/worker/events/stream
  • Не буферизуйте ответ: передавайте его по мере поступления. Его тип содержимого — text/event-stream.
  • Позвольте ответу оставаться открытым: он может оставаться открытым до часа. Если у прокси есть таймаут бездействия, сделайте его значительно больше 15 секунд, поскольку по умолчанию Anthropic отправляет keep-alive как минимум с такой частотой.

Затем запустите новую сессию и проверьте, что Claude отвечает. Увеличение таймаутов runner'а не поможет: вы лишь дольше ждёте, прежде чем runner остановит сессию.

Подтвердите причину по логу runner'а

Эти шаги предназначены для runner'ов на Claude Code v2.1.287 или новее с уровнем логирования по умолчанию --log-level info. Более старые runner'ы записывают эти строки позже или не записывают вовсе.

  1. Дождитесь истечения --startup-timeout-min, по умолчанию 15 минут. Затем runner останавливает сессию и записывает в лог строку, которая содержит no child output for и заканчивается на — releasing.
  2. Найдите в логе runner'а SDKStartup: SSE. Эти строки следуют за строкой finished: interrupted, через несколько строк после строки из шага 1. У большинства из них нет временной метки, поэтому сборщик логов может показывать их в другом месте.
  3. Найдите строку в этой таблице:
Строка лога содержит Что делать
SSE connect failed (no response in 30s) или SSE liveness timeout Проверьте, что прокси пересылает запрос, передаёт заголовки и данные ответа по мере поступления и не разрывает соединение
SSE connect failed (HTTP и код состояния Проверьте собственный лог прокси. Если прокси сам ответил на запрос, пропустите запрос через него.
SSE connect failed ( и любой другой текст Текст в скобках — это ошибка. Найдите то же соединение в собственном логе прокси.
SSE stream ended by server, снова и снова Проверьте ограничение прокси по времени для долгоживущих ответов. Одна такая строка — это нормально.
SSE reconnected after Ищите после неё одну из других строк. Эта строка не означает, что что-либо дошло до сессии.

Если в логе нет ни одной из этих строк или в логе прокси нет признаков проблемы, сообщите о проблеме.

Остановите повторные попытки сессии

Сессия сохраняет сообщение как ожидающее, и runner, который может обслуживать владельца сессии, снова принимает сессию. Пока вы не измените прокси, сессия каждый раз ждёт так же. Чтобы это прекратить, архивируйте сессию в claude.ai/code.

Что дальше

  • Customize sessions: скрипты-обертки, lifecycle hooks, on-demand runners, MCP servers и разрешения
  • Test end to end: проверьте новый образ runner из CI перед продвижением
  • Reference: каждый флаг CLI, переменная окружения и метрика