Every agent CLI hive drives is baked into the images at a fixed version:
| CLI | Pin (ARG) |
Source of truth | Verified by |
|---|---|---|---|
| Claude Code | CLAUDE_CODE_VERSION |
npm @anthropic-ai/claude-code |
npm (no download hash) |
| Codex | CODEX_VERSION |
npm @openai/codex |
npm |
| Copilot | COPILOT_VERSION |
npm @github/copilot |
npm |
| pi | PI_VERSION (hub), PI_CODING_AGENT_VERSION (contributor) |
npm @earendil-works/pi-coding-agent |
npm |
| Goose | GOOSE_VERSION, GOOSE_SHA256_{AMD64,ARM64} |
GitHub releases aaif-goose/goose |
SHA-256 per arch |
| Antigravity (agy) | AGY_VERSION + AGY_BUILD, AGY_SHA512_* (hub); AGY_VERSION=<ver>-<build>, AGY_SHA256_* (contributor) |
Google's updater artifact, as tracked by the Homebrew antigravity-cli cask |
SHA-512 (hub) / SHA-256 (contributor) per arch |
| Oh My Pi (omp) | OMP_VERSION, OMP_SHA256_* |
GitHub releases can1357/oh-my-pi |
SHA-256 per arch, cross-checked with SHA256SUMS.txt |
| Muse Code | MUSE_VERSION, MUSE_SHA256_* |
api.meta.ai/muse-code/channels/muse-stable and its release manifest |
SHA-256 per arch, cross-checked with the manifest |
| bobshell (bob) | BOBSHELL_VERSION, BOBSHELL_SHA256_* |
bobshell2-version.txt on IBM COS |
SHA-256, cross-checked with the vendor .tgz.sha256 |
| gh | GH_VERSION, GH_SHA256_* (hub only) |
GitHub releases cli/cli |
SHA-256 per arch, cross-checked with gh_<v>_checksums.txt |
The pins live in src/Dockerfile (hub and spoke image) and
src/Dockerfile.contributor (containerized contributors), and the two must
agree: bin/test_backend_smoke.sh section A2/A3 fails when the claude or
codex pins differ. Pinning is deliberate (#2903):
a floating @latest makes builds non-reproducible, and the per-arch digest
makes a tampered download fail the build instead of running.
Nothing else moves these pins. .github/dependabot.yml covers Go modules,
GitHub Actions, FROM tags and the proxy's npm tree, none of which reach an
ARG *_VERSION line. Left alone the pins drift until a user hits a wall,
which is what #8417 was:
Claude Code 2.1.226 refusing claude-opus-5-5 six weeks after 2.1.280 shipped.
Letting the CLIs update themselves at runtime is the wrong fix: it undoes the
reproducible, hash-checked build, the update is lost on every container
restart (the CLIs live in the image layer), agents on one hive end up on
different versions, and a TUI change reaches production with no test in
between while hive reads those TUIs (pane scraping, onboarding dialogs,
working-state text). The contributor path already writes
autoUpdates: false into ~/.claude.json for these reasons.
Once a day (23 5 * * *) and on workflow_dispatch, one job per CLI checks
for a new release. The daily automation opens PRs only for major or minor
version changes. Patch-only releases (X.Y.Z -> X.Y.W, including suffixes
such as prereleases or agy's build id) are treated as current and skipped so
routine patch churn does not create PRs. To force a patch bump, dispatch the
workflow with include_patch=true (or run the script with
HIVE_PIN_INCLUDE_PATCH=1).
For eligible major/minor changes, the job:
- Resolve.
src/scripts/cli-pin-bump.sh bump <cli>asks the CLI's source of truth for the latest release and, for download-verified CLIs, downloads every per-arch artifact and recomputes its digest. Where the vendor publishes a digest (omp, gh, Muse, bobshell, the agy cask) the computed value must match it or the job fails. A digest is never copied from a page and never guessed: the script refuses to write anything that is not a 64- or 128-hex-character value, and it validates every value before the first edit so a Dockerfile is never left half-updated. - Edit. Both Dockerfiles are rewritten in place (the
ARGlines only), plussrc/pkg/config/dockerfile_contributor_test.go, which asserts pi's exactARGline. - Smoke before the PR exists. The job builds
src/Dockerfilefor linux/amd64 with the new pin and runs<cli> --versioninside the image (for claude also an unauthenticatedclaude --help). A CLI whose install layer or startup path broke never becomes a PR. - Open one PR per CLI on branch
cli-pin-bump/<cli>against the base branch (v5by default; the dispatch inputbasepicks another). The PR body carries the resolvedKEY=VALUEprovenance and the smoke transcript. An open PR for the same CLI is refreshed rather than duplicated, and if it already carries the same target version the job does nothing.
Labels on a bump PR:
security- admits the PR through the v4 feature-freeze gate (.github/workflows/v4-freeze-gate.yml), so a CLI fix can still land on a frozen line.dependencies,no-changelog- the dependabot convention; a pin bump is dependency churn and needs no changelog fragment.needs-human- added only for a major version change. Minor bumps follow the repo's normal merge-on-green path; patch bumps are skipped unless an operator explicitly forces one, except for patch-eligible CLIs (below).
Most CLIs ship features on major/minor releases, so patch-only bumps are
skipped. Claude Code is patch-eligible: it ships new models as 2.1.x
patches (for example Sonnet 5.5 needs 2.1.284), so skipping patches left the
image unable to launch models Hive lists. cli-pin-bump.sh bumps patch
releases of the CLIs in HIVE_PIN_PATCH_ELIGIBLE (default claude; set it to
empty to restore the strict rule). Add a CLI there when it starts shipping
models in patches.
A pin change can leave Hive's model lists behind. Two layers cover it:
src/scripts/check-cli-model-lists.sh(self-test:test-check-cli-model-lists.sh) fails when a model listed insrc/scripts/cli-model-requirements.txt(CLI, model id, first version that accepts it) is missing from the allowlist of a CLI pinned at or above that version, and when the "version pinned ... (X" / "matches codex X" comments incli_models.godiffer from the Dockerfile pin. Add a requirements line when a CLI release introduces a model. The check runs in the workflow'sselftestjob on PRs touching the pins or lists, and its output goes in every bump PR. It warns instead of failing until the lists are current; setHIVE_MODEL_CHECK_STRICT=1to make drift fail.- Each bump PR body carries a checklist for what cannot be automated:
allowlists/fallbacks,
static/index.html, andsrc/pkg/tokens/pricing.go.
The PR then runs the normal gates. The docker workflow builds both images for
linux/amd64 and linux/arm64, where every hash-verified layer runs
sha256sum -c / sha512sum -c; the backend smoke's
pinned lane picks the new CLI up on its next scheduled run.
Credentials: the workflow opens PRs with GITHUB_TOKEN under
pull-requests: write, the same path v5-topup.yml uses, and pushes with
TOPUP_PUSH_TOKEN when that optional PAT is set (the bump never touches
.github/workflows/, so GITHUB_TOKEN can push it too). No new secret.
# what is pinned right now
src/scripts/cli-pin-bump.sh current codex
# resolve the latest release and its digests without editing anything
src/scripts/cli-pin-bump.sh resolve goose
# resolve + edit both Dockerfiles for major/minor changes; patch-only changes
# print CHANGED=false/SKIPPED=patch unless HIVE_PIN_INCLUDE_PATCH=1 is set
src/scripts/cli-pin-bump.sh bump clauderesolve and bump need curl, jq and sha256sum/shasum; set
GH_TOKEN to avoid the anonymous GitHub API rate limit. apply <cli> <kv>
edits from a saved KEY=VALUE file with no network at all, which is what the
self-test (src/scripts/test-cli-pin-bump.sh, run by the workflow's
selftest job on any PR that touches the script or workflow) exercises.
To bump one CLI on demand from the Actions tab, dispatch CLI pin bump
with cli=<name>; all checks every pin. Leave include_patch=false for the
normal major/minor-only policy, or set include_patch=true to force a
patch-only bump PR.
- Add the
ARGlines to both Dockerfiles following an existing download-verified layer (version plus per-arch digest,sha256sum -cbefore install, never acurl | shinstaller). - Add a
resolve_<cli>function and theapply_cli/hub_version_argcases tosrc/scripts/cli-pin-bump.sh, and a fixture line plus assertions tosrc/scripts/test-cli-pin-bump.sh. - Add the name to the workflow's matrix and dispatch
options, and to theCLISlist in the script.
- The workflow does not diff each CLI's full model catalog before and after a
bump (catalogs baked into binaries cannot be read at build time; see Model
lists above). The dashboard's live probes (
codex app-servermodel/list,agy models,omp models --json) remain the source of truth on a running hive; a static-catalog refresh, when one is needed, is a hand edit tocodexStaticModelsinsrc/pkg/dashboard/cli_models.goand itsstatic/index.htmlmirror. - No fast lane from a "this model needs version X" runtime error
(#8418) to a dispatch of
this workflow. Dispatching by hand with
cli=<name>is the interim path. KILO_CLI_VERSIONandCOPILOT_SDK_VERSIONare pinned but not bumped by this workflow.