Skip to main content
Самостоятельно размещаемые окружения находятся в публичной бета-версии на планах 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, чтобы узнать, где должны находиться эти гранты.
Если в вашей организации включён список разрешённых IP-адресов, добавьте публичные адреса исходящего трафика ваших runners и контейнеров сессий в список разрешённых перед их запуском. Если вы используете on-demand runners, добавьте также адрес хоста оркестратора. Не полагайтесь на список разрешённых как на сетевой контроль для трафика runner или сессий. Вместо этого применяйте default-deny egress на границе вашей собственной сети.

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

Runner и порожденные им дочерние сеансы устанавливают исходящие соединения с хостами ниже. Ограничьте исходящий трафик контейнера сеанса этими хостами и конкретными внутренними сервисами, которые сеансам нужно достичь; Default-deny egress охватывает как и почему. Эти хосты всегда требуются: Runner, использующий --use-anthropic-git-proxy, направляет свой git-трафик к github.com через api.anthropic.com, поэтому ему не нужен путь к git-хосту github.com. Этот путь всё ещё нужен, если вы устанавливаете --push-outcome-on-release или выполняете push из хука post-session. Требуются ли эти хосты, зависит от вашей конфигурации: Эти хосты не нужно добавлять в список разрешённых хостов для трафика 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, чтобы конфигурация применялась независимо от того, от имени какого пользователя работает процесс раннера:
Без идентичности 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 не открывает диалог входа.
  • Раннер очищает core.askPass, поэтому, если вы используете askpass-помощник, задайте его через переменную окружения GIT_ASKPASS.
Если ваш git-хост отклоняет учётные данные или вы их не настроили, раннер делает несколько повторных попыток, а затем прерывает подготовку репозитория, если это репозиторий, в который сессия отправляет результаты. О том, когда раннер вместо этого пропускает репозиторий, который сессия только читает, рассказано в разделе Устранение неполадок. Раннер не передаёт эти настройки в окружение сессии. Храните любую программу, указанную в GIT_SSH_COMMAND или GIT_ASKPASS, там, куда сессии не могут писать, — так же, как контрольный список усиления защиты требует для каталога хуков и скрипта-обёртки. То же относится к любому ключу или файлу в командной строке этой программы. Собственный git раннера запускает эту программу при клонировании или fetch. Если каталоги извлечения принадлежат другому uid, чем процесс раннера, git отказывается с ними работать; добавьте 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-прокси.
При заданном --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-прокси.
Рецепты 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-прокси:
При запуске раннер выводит 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, удалите её оттуда. В оболочке сбросьте её:
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 отправляет свои 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
Если образ включает GitHub CLI, сессии используют его.

Доверие частному центру сертификации при использовании управляемого 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.

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

Anthropic не публикует предварительно построенный образ runner. Постройте свой собственный вокруг бинарного файла claude, наслаивая любой набор инструментов, который нужен вашим репозиториям: языковые среды выполнения, компиляторы, менеджеры пакетов и MCP sidecars. Рецепты ниже используют --capacity 4, поэтому один контейнер обслуживает до четырех одновременных сеансов от одного заблокированного владельца. Это не обеспечивает изоляцию контейнера для каждого сеанса в разделе усиления безопасности: перед подключением окружения к production-системам либо запустите рецепты на --capacity 1 с одним контейнером на сеанс, либо используйте on-demand runners, которые также держат секрет окружения вне хостов, запускающих сеансы. Если вы добавите Anthropic git proxy к одному из этих рецептов, также измените --capacity на 1. Этот Dockerfile является минимальной отправной точкой:
Замените 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 или позже. Постройте образ, затем отправьте его в ваш реестр и ссылайтесь на него в рецептах ниже:
Подстановка команды ищет текущий номер выпуска 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:
Сборки и тесты обычно являются самой большой и наиболее переменной частью нагрузки сеанса, поэтому запустите репрезентативную сборку вашего репозитория, измерьте его пиковый 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: Если вы не задали 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.
Развертывание выше живет в пространстве имен claude-runners. Сначала создайте пространство имен:
Создайте поддерживающий Secret из локального файла, содержащего значение, которое вы скопировали на шаге Copy environment key в пользовательском интерфейсе администратора, чтобы секрет никогда не появлялся в истории вашей оболочки. Запустите (umask 077 && cat > ./environment-secret), вставьте секрет, нажмите Enter, затем Ctrl-D. Затем создайте Secret и удалите файл:

Docker Compose

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

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

После SIGTERM runner нужно время, чтобы корректно завершить свои сессии, прежде чем ваш оркестратор его убьёт. Сколько именно, он записывает в лог при запуске, в строке, которая при настройках по умолчанию содержит This runner needs up to 80s. Установите таймаут остановки вашего оркестратора не меньше этого количества секунд: terminationGracePeriodSeconds в Kubernetes, stop_grace_period в Docker Compose или эквивалент на вашей платформе. В Kubernetes значение по умолчанию составляет 30 секунд, поэтому без этой настройки он может остановить pod до того, как runner завершит работу. При получении 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. В разделе Время завершения работы перечислено, что увеличивает эти числа.
Если таймаут остановки истекает во время ожидания, ваш хост убивает 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 остаётся заблокированным за владельцем этой сессии. Если облачная запланированная задача вашей команды запускает новую сессию при каждом выполнении, её простаивающие сессии могут занять все слоты. Чтобы проверить это, посмотрите claude_code_self_hosted_runner_session_idle_seconds в Prometheus metrics каждого runner. У каждой сессии, которую удерживает runner, есть собственный ряд: 0, пока сессия занята, в противном случае — количество секунд с момента перехода в простой. Выберите допустимое время простоя: дольше пауз, которые люди делают посреди задачи, и достаточно короткое, чтобы каждое выполнение освобождалось до начала следующего выполнения запланированной задачи. Сравните ряды с этим временем. Чтобы освобождать простаивающие сессии, запускайте каждый runner с --release-idle-session-min и этим временем в минутах, например --release-idle-session-min 30. Тогда runner освобождает любую сессию, которая простаивает так долго, независимо от того, запустила её запланированная задача или человек.

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

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

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

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 завершает его, и любая работа работающего хода потеряна. Ход, ожидающий одобрения, запрошенного изнутри работающего вызова инструмента, - это один способ, которым сеанс переживает окно.
Выпущенный сеанс возобновляется с свежего клона, поэтому работа, которую он не отправил, потеряна в любом случае; см. Resumed sessions lose unpushed work. До v2.1.260, runner завершал каждый сеанс на лимите, после ожидания максимум окна благодати для работающего хода, чтобы закончиться. Установите флаг выше вашего самого длинного ожидаемого сеанса, такой как --kill-session-after-min 480 для 8 часов. Чтобы освободить слоты из разговоров, которые становятся неактивными, используйте вместо этого --release-idle-session-min.

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

  • Возобновлённые сессии теряют неотправленную работу: новый runner заново клонирует репозиторий с его начальной ветки, поэтому работа, которую сессия не отправила, теряется.
    • Чтобы сохранить закоммиченную работу: задайте --push-outcome-on-release на каждом runner в окружении, поскольку runner без этого флага возобновляет сессию с её начальной ветки. Runner с этим флагом перед освобождением по возможности отправляет (push) ветки результатов сессии, и возобновлённая сессия начинается с этих коммитов. Для push используются собственные учётные данные git хоста runner, в том числе на runner, который использует управляемый Anthropic git.
    • С хуком checkout: репозитории, извлечённые с помощью хука жизненного цикла checkout, не отправляются. Вместо этого сохраняйте их снимки из хука post-session.
    • Перед включением флага: ограничьте круг тех, кто может выполнять push в refs claude/* на исходном удалённом репозитории. При возобновлении runner получает ранее отправленную ветку, не проверяя, кто её отправил.
    • Незакоммиченные изменения всё равно теряются: чтобы побудить Claude делать коммит и push по завершении ответа, см. Побуждение сессий отправлять свою работу
  • Команда, прерванная из-за потери runner, может выполниться повторно: когда сессия возобновляется на другом runner, Claude видит прерванный вызов инструмента с результатом, сообщающим, что его исход неизвестен.
  • Репозиторий, добавленный в середине сессии, может не клонироваться: Claude клонирует его с помощью git clone по HTTPS. На runner без --use-anthropic-git-proxy клонирование завершается ошибкой аутентификации git, если ничто на хосте не может прочитать репозиторий. По возможности выбирайте все необходимые сессии репозитории при её создании.
  • Некоторые коннекторы не появляются в самостоятельно размещаемых сеансах: коннектор, который вы еще не подключили в параметрах claude.ai, не указан в самостоятельно размещаемом сеансе, и сеанс не будет вас приглашать подключить его. Подключите его в параметрах сначала, затем запустите свежий сеанс. Добавление коннектора в уже работающий сеанс тоже не делает его инструменты доступными для Claude; запустите свежий сеанс, чтобы подхватить недавно добавленный коннектор.
  • Когда кто-то архивирует или удаляет сессию либо освобождает её runner из меню сессии: не рассчитывайте на push через --push-outcome-on-release. Чтобы сохранить коммиты в этих случаях, отправляйте HEAD в резервную ветку из хука post-session. Если вы берёте за основу приведённый там пример, сделайте так, чтобы он выполнял push, даже когда незакоммиченных изменений нет, поскольку он выполняет push только после коммита-снимка.

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

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

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

Для пошаговой диагностики запустите подкоманду doctor на хосте runner’а. Подкоманда doctor запускает интерактивную сессию Claude Code с подключёнными логами и состоянием runner’а. Сначала войдите в систему с помощью claude auth login на этом хосте, чтобы сессия могла запрашивать данные о вашем окружении, его runner’ах и его сессиях в очереди. Без этого входа, например когда хост проходит аутентификацию с помощью API-ключа, она ограничена локальным эндпоинтом проверки работоспособности, метриками и логом runner’а, причём лог она читает, только если вы запустили runner с --log-file.
Частые проблемы:
  • 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 к команде, которая его запускает, перед любым &, переводящим её в фоновый режим:
    Собственный вывод вашего скрипта по-прежнему идёт туда же, куда и раньше, а то, что 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 такая сессия запускалась в пустом каталоге.
  • Сессия запускается без одного из своих репозиториев: на runner’е без хука checkout git-хост может отклонить проверку доступа runner’а к репозиторию, из которого сессия только читает. Тогда runner пропускает этот репозиторий, записывает в лог строку [runner:warn] could not access context source с указанием отказа и запускает сессию с остальными репозиториями. Runner пропускает только явный отказ: хост отвечает, что репозиторий не найден, git не находит учётных данных для хоста или аутентификация не проходит. Сбой сети, таймаут или HTTP 403 по-прежнему приводят к сбою запуска сессии, как и отказ для репозитория, в который сессия отправляет результаты. Runner по-прежнему намеренно завершает сбоем сессию, которую пропуск оставил бы вообще без репозитория. С --use-anthropic-git-proxy runner пропускает только репозиторий, доступ к которому отклоняет сам git-прокси. Проверка доступа выполняется заново при каждом запуске сессии на runner’е, поэтому, как только git-идентичность runner’а получит доступ на чтение, следующий запуск клонирует репозиторий. До v2.1.274 каждый из этих отказов приводил к сбою запуска сессии.
  • Сессия не запускается с 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: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’а, в логах контейнеров вашей платформы или в файле, заданном с помощью --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:
    Если последний запуск был неудачным, строка [runner:fatal] или error: находится среди последних строк вывода. Чтобы прочитать последний запуск другого пода, укажите этот под вместо deploy/claude-runner.
  • Docker и Docker Compose: рецепт Compose на этой странице не требует изменений. С restart: always Docker ждёт всё дольше перед каждым перезапуском контейнера, который продолжает завершаться, до верхнего предела. Замените <container> на имя контейнера в команде ниже, которая показывает, сколько раз Docker перезапускал контейнер:
    Команда выводит число. Если число продолжает расти, 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 отправляет сессии её сообщения через один потоковый ответ, который остаётся открытым. Если ваш прокси буферизует его или закрывает, прежде чем что-либо передать, первое сообщение так и не доходит. Другие запросы сессии при этом выполняются успешно. Чтобы это исправить, измените то, как прокси обрабатывает этот запрос:
  • Не буферизуйте ответ: передавайте его по мере поступления. Его тип содержимого — 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. Найдите строку в этой таблице:
Если в логе нет ни одной из этих строк или в логе прокси нет признаков проблемы, сообщите о проблеме.

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

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

Что дальше

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