chore(agent-tools,compat): bump the Claude Code pin 2.1.220 → 2.1.221 — live re-verification, closing #235's owed gate #274

Merged
dominik.polakovics merged 1 commit from afk/273 into main 2026-08-04 05:00:53 +02:00

Closes #273

Moves the agent-tools Claude Code pin to 2.1.221, with the compat re-verification the bump procedure demands done first. The re-verification is the work; the two-line versions.env edit is the trivial part.

The live gate is closed — #235's owed work, done here

The issue made #235 a hard prerequisite: without a green live suite on the outgoing pin, you cannot tell 2.1.221 drift from the pre-existing #56 assertion bug. Both halves are done:

  • Work item 1 — the two stale assertions in live_recipes_test.go still expected a resolved dialog to surface as a demoted tool chip; issue #56 made it a dialog message carrying its recorded Outcome. Fixed against dialogoutcome.go's actual shape. Nothing earlier in either test was weakened — the recipe driving, pane polling and recorded-answer comparison are byte-identical.
  • Work item 2 — the suite was run against the outgoing 2.1.220 binary first, then 2.1.221, and the outcomes diffed:
LAB_COMPAT_LIVE=1 go test ./internal/compat/ -run Live -v
2.1.220 (baseline) 2.1.221 (candidate)
TestCompat_LiveTranscripts_resolutionShapes PASS PASS
TestCompat_Live_authStatusParses PASS PASS
TestCompat_Live_authLogout PASS PASS
TestCompat_Live_configDirResolution PASS PASS
TestCompat_Live_askUserQuestionRecipe PASS (10.07s) PASS (9.53s)
TestCompat_Live_exitPlanModeApproval PASS (13.46s) PASS (13.91s)
skips 0 0

Zero skips on both — a skipped run is not a pass, and these carry four skip-gates. The two recipe tests drive a real dialog through the production DialogKeystrokes/AnswerDialog path and read the recorded answers back out of the transcript; both spawn without --remote-control, so this is live evidence on §12's arm.

Why this version

2.1.221 fixes a Bash tool permission-check bypass where zsh could execute hidden commands in [[ ]] regex conditionals without prompting — in --permission-mode auto, the exact mode lab spawns, on runs that are unattended by design.

Reachability is conditional, and the record says so rather than overstating it: lab ships no shell (Containerfile.claude is FROM scratch), grep -rn zsh over this repo returns zero hits, and the session shell comes from the operator-supplied dev image lab does not pin. Bash-only dev image → not reachable; zsh dev image → reachable, blast radius one container instance and its worktree. Defense in depth, not an incident. The PowerShell companion fix cannot reach lab at all.

Pin verification

The digest was re-derived from the downloaded bytes, not trusted from the issue:

sha256(claude-2.1.221-linux-x64-musl) = 15b068e06eafff9b64583b46cdc065ac18b0d0d13950c2a83c6ee854f301a32f

…matching Anthropic's per-version manifest.json, and verifying under build.sh's own sha256sum -c check. The artifact was then executed through Alpine's musl loader — the same proof Containerfile.claude:51's pre-patch sanity run makes — and reports 2.1.221 (Claude Code).

§10 re-scrape — no drift from this bump, but three pre-existing stale rows found

Re-extracted from the sha256-verified musl artifact itself and A/B'd against 2.1.220 with the same extractor.

  • Nothing drifted between 2.1.220 and 2.1.221. The whole type-literal registry is byte-identical: 105 distinct names, zero added, removed, or reworded.
  • /status is byte-identical, despite the changelog's session-kind behavior change. The behavior moved; the string did not — which is exactly what the verbatim pin exists to catch, so it was checked rather than assumed.
  • Three pinned descriptions were already stale, identically on 2.1.220 — the 2.1.220 pass re-checked only the ten chat-safe rows, so it never looked. All three are curated-out rows, so nothing chat-facing ever served wrong text. commands.go updated with the bundle evidence quoted in the commit message: feedback, usage-credits, and doctor.
  • /doctor is a shape change: it now carries an operator-facing menuDescription plus a description that has become a long model-facing prompt. Its curated-out reason was also factually wrong ("interactive diagnostics UI" — it runs as a model turn now) and is corrected; the false verdict survives on the invasive-config ground.

Method lesson recorded in §10 for the next sweep: a forward regex over description:"…" reports a renamed key as an absent row. The check that actually settles drift is the reverse direction — grep the raw binary for each pinned string. That is what caught all three.

/fork — explicit written ChatSafe decision: false

It is genuinely enabled and visible in every lab session (isEnabled:()=>!Nb(), and Nb() needs CLAUDE_CODE_COORDINATOR_MODE, which lab does not set), so the verdict is load-bearing rather than moot. Three independent grounds: transcript identity (the same hazard that disqualifies /resume), worktree leakage (every lab instance already is a linked worktree; a fork cutting its own leaves a checkout lab never registered and will never prune), and an unsupervised background session. /subtask inherits it — same spawn path.

The standing catalog gap, now measured

The pinned table is 33 rows against a 105-name registry. That delta is pre-existing (byte-identical at 2.1.220), not this bump's drift, and it is now measured and curated in §10 as deliberately omitted, with the add-candidates named (help is the strongest). Widening the served composer surface changes what every operator sees — a product decision that wants its own issue, not a pin bump.

Verification run

  • go build -tags ui ./cmd/... — OK
  • go test -tags ui ./... — all pass
  • go test ./... — all pass
  • golangci-lint run ./... (v2.12.2, matching CI) — 0 issues
  • gofmt -l — clean
  • TestCompat_BuiltinCommands_pinned — PASS

One gap, stated plainly

The container build and injection smoke test were not run locally — this host has no container runtime (no podman/docker, no root). containers/agent-tools/build.sh claude and smoke-test.sh could not execute here.

What covers it: the digest was re-derived and verifies under build.sh's exact check, the artifact was executed through the same musl loader the image bundles, and this PR is path-gated onto the agent-tools CI workflow (versions.env lives under containers/**), whose smoke job builds both images and runs the injection smoke test against debian:stable-slim and alpine. That CI leg is the authority on the image; please confirm it goes green before merging.

Also not re-driven, so not claimed anywhere in the record: the §12 pending-tool_use flush A/B, the §2 deep-link registry (an ad-hoc probe failed identically on both versions — a broken probe, not drift), and the §3b by-hand credential-refresh recipe.

Closes #273 Moves the agent-tools Claude Code pin to **2.1.221**, with the compat re-verification the bump procedure demands done *first*. The re-verification is the work; the two-line `versions.env` edit is the trivial part. ## The live gate is closed — #235's owed work, done here The issue made #235 a hard prerequisite: without a green live suite on the outgoing pin, you cannot tell 2.1.221 drift from the pre-existing #56 assertion bug. Both halves are done: - **Work item 1** — the two stale assertions in `live_recipes_test.go` still expected a resolved dialog to surface as a demoted **tool chip**; issue #56 made it a **dialog message carrying its recorded `Outcome`**. Fixed against `dialogoutcome.go`'s actual shape. Nothing earlier in either test was weakened — the recipe driving, pane polling and recorded-answer comparison are byte-identical. - **Work item 2** — the suite was run against the **outgoing 2.1.220 binary first**, then 2.1.221, and the outcomes diffed: ``` LAB_COMPAT_LIVE=1 go test ./internal/compat/ -run Live -v ``` | | 2.1.220 (baseline) | 2.1.221 (candidate) | |---|---|---| | `TestCompat_LiveTranscripts_resolutionShapes` | PASS | PASS | | `TestCompat_Live_authStatusParses` | PASS | PASS | | `TestCompat_Live_authLogout` | PASS | PASS | | `TestCompat_Live_configDirResolution` | PASS | PASS | | `TestCompat_Live_askUserQuestionRecipe` | PASS (10.07s) | PASS (9.53s) | | `TestCompat_Live_exitPlanModeApproval` | PASS (13.46s) | PASS (13.91s) | | **skips** | **0** | **0** | **Zero skips on both** — a skipped run is not a pass, and these carry four skip-gates. The two recipe tests drive a real dialog through the production `DialogKeystrokes`/`AnswerDialog` path and read the recorded answers back out of the transcript; both spawn **without** `--remote-control`, so this is live evidence on §12's arm. ## Why this version 2.1.221 fixes a **Bash tool permission-check bypass** where zsh could execute hidden commands in `[[ ]]` regex conditionals without prompting — in `--permission-mode auto`, the exact mode lab spawns, on runs that are unattended by design. Reachability is **conditional**, and the record says so rather than overstating it: lab ships no shell (`Containerfile.claude` is `FROM scratch`), `grep -rn zsh` over this repo returns zero hits, and the session shell comes from the operator-supplied dev image lab does not pin. Bash-only dev image → not reachable; zsh dev image → reachable, blast radius one container instance and its worktree. Defense in depth, not an incident. The PowerShell companion fix cannot reach lab at all. ## Pin verification The digest was **re-derived from the downloaded bytes**, not trusted from the issue: ``` sha256(claude-2.1.221-linux-x64-musl) = 15b068e06eafff9b64583b46cdc065ac18b0d0d13950c2a83c6ee854f301a32f ``` …matching Anthropic's per-version `manifest.json`, and verifying under `build.sh`'s own `sha256sum -c` check. The artifact was then **executed** through Alpine's musl loader — the same proof `Containerfile.claude:51`'s pre-patch sanity run makes — and reports `2.1.221 (Claude Code)`. ## §10 re-scrape — no drift from this bump, but three pre-existing stale rows found Re-extracted from the sha256-verified **musl artifact itself** and A/B'd against 2.1.220 with the same extractor. - **Nothing drifted between 2.1.220 and 2.1.221.** The whole type-literal registry is byte-identical: 105 distinct names, zero added, removed, or reworded. - **`/status` is byte-identical**, despite the changelog's session-kind behavior change. The behavior moved; the string did not — which is exactly what the verbatim pin exists to catch, so it was checked rather than assumed. - **Three pinned descriptions were already stale**, identically on 2.1.220 — the 2.1.220 pass re-checked only the *ten chat-safe rows*, so it never looked. All three are curated-out rows, so **nothing chat-facing ever served wrong text**. `commands.go` updated with the bundle evidence quoted in the commit message: `feedback`, `usage-credits`, and `doctor`. - `/doctor` is a **shape** change: it now carries an operator-facing `menuDescription` plus a `description` that has become a long *model-facing* prompt. Its curated-out *reason* was also factually wrong ("interactive diagnostics UI" — it runs as a model turn now) and is corrected; the `false` verdict survives on the invasive-config ground. Method lesson recorded in §10 for the next sweep: a forward regex over `description:"…"` reports a **renamed key as an absent row**. The check that actually settles drift is the reverse direction — grep the raw binary for each pinned string. That is what caught all three. ## `/fork` — explicit written ChatSafe decision: `false` It is genuinely **enabled and visible** in every lab session (`isEnabled:()=>!Nb()`, and `Nb()` needs `CLAUDE_CODE_COORDINATOR_MODE`, which lab does not set), so the verdict is load-bearing rather than moot. Three independent grounds: transcript identity (the same hazard that disqualifies `/resume`), **worktree leakage** (every lab instance already *is* a linked worktree; a fork cutting its own leaves a checkout lab never registered and will never prune), and an unsupervised background session. `/subtask` inherits it — same spawn path. ## The standing catalog gap, now measured The pinned table is 33 rows against a 105-name registry. That delta is **pre-existing** (byte-identical at 2.1.220), not this bump's drift, and it is now measured and curated in §10 as deliberately omitted, with the add-candidates named (`help` is the strongest). Widening the served composer surface changes what every operator sees — a product decision that wants its own issue, not a pin bump. ## Verification run - `go build -tags ui ./cmd/...` — OK - `go test -tags ui ./...` — all pass - `go test ./...` — all pass - `golangci-lint run ./...` (v2.12.2, matching CI) — **0 issues** - `gofmt -l` — clean - `TestCompat_BuiltinCommands_pinned` — PASS ## One gap, stated plainly **The container build and injection smoke test were not run locally** — this host has no container runtime (no podman/docker, no root). `containers/agent-tools/build.sh claude` and `smoke-test.sh` could not execute here. What covers it: the digest was re-derived and verifies under `build.sh`'s exact check, the artifact was executed through the same musl loader the image bundles, and **this PR is path-gated onto the `agent-tools` CI workflow** (`versions.env` lives under `containers/**`), whose `smoke` job builds both images and runs the injection smoke test against `debian:stable-slim` and `alpine`. That CI leg is the authority on the image; please confirm it goes green before merging. Also not re-driven, so not claimed anywhere in the record: the §12 *pending*-`tool_use` flush A/B, the §2 deep-link registry (an ad-hoc probe failed identically on **both** versions — a broken probe, not drift), and the §3b by-hand credential-refresh recipe.
chore(agent-tools,compat): bump the Claude Code pin 2.1.220 -> 2.1.221
All checks were successful
agent-tools / publish (pull_request) Has been skipped
agent-tools / smoke (pull_request) Successful in 13m53s
ci / native (pull_request) Successful in 14m11s
e42fffe209
Re-verify the compat record against a real 2.1.221 binary FIRST, then move
the version + sha256 together, as versions.env:11-16 and docs/ops.md
§ "Agent-tools images" require. The re-verification is the work here; the
two-line versions.env edit is the trivial part.

Why this version: 2.1.221 fixes a Bash tool permission-check bypass where
zsh could execute hidden commands in `[[ ]]` regex conditionals without
prompting — in `--permission-mode auto`, the exact mode lab spawns, on runs
that are unattended by design. Reachability is conditional (lab ships no
shell; the session shell comes from the operator-supplied dev image, and
`grep -rn zsh` over this repo returns zero hits), so it is defense in depth
rather than an incident. The reasoning is recorded in compat §1 so the next
sweep does not re-derive it. The companion PowerShell fix cannot reach lab.

The live gate that #235 recorded as owed is now CLOSED. The suite was run
against the OUTGOING 2.1.220 binary first, so 2.1.221 had a baseline to diff
against rather than an unknown:

    LAB_COMPAT_LIVE=1 go test ./internal/compat/ -run Live -v

Six live tests, ZERO skips, all passing on BOTH binaries, with identical
verdicts — including the two that drive a real dialog through the production
DialogKeystrokes/AnswerDialog path and read the recorded answers back out of
the transcript. Both recipe tests spawn without --remote-control, so this is
live evidence on §12's arm.

Pin verification: the linux-x64-musl artifact (the image input itself) had
its sha256 RE-DERIVED from the downloaded bytes rather than trusted —
15b068e06eafff9b64583b46cdc065ac18b0d0d13950c2a83c6ee854f301a32f, matching
Anthropic's per-version manifest.json — and was then executed through
Alpine's musl loader, the same proof Containerfile.claude's pre-patch sanity
run makes; it reports "2.1.221 (Claude Code)".

§10 re-scrape: nothing drifted between 2.1.220 and 2.1.221 — the whole
type-literal command registry is byte-identical (105 distinct names, zero
added, removed, or reworded). /status in particular is byte-identical
despite the changelog's session-kind behavior change, which is precisely
what the verbatim pin exists to catch.

But the re-scrape did find three pinned descriptions that were already
stale, identically on 2.1.220 — a pre-existing miss, because the 2.1.220
pass re-checked only the ten chat-safe rows. All three are curated-out rows,
so nothing chat-facing ever served wrong text. Bundle evidence, verbatim:

    name:"feedback",description:"Send feedback to Anthropic or report a bug",argumentHint:"[report]"
      was pinned: "Submit feedback, report a bug, or share your conversation"  (0 occurrences in either binary)

    usage-credits: "Configure usage credits or request them from your admin when you hit a limit"
      was pinned: "Configure usage credits to keep working when you hit a limit"

    name:"doctor",aliases:["checkup"],...,menuDescription:"Health-check your setup and fix issues: installation,
      unused extensions, duplicated or bloated memory files, slow hooks, updates, permissions",description:"..."
      was pinned: "Diagnose and verify your Claude Code installation and settings"  (absent from the bundle entirely)

/doctor is a shape change, not just a wording one: it now carries an
operator-facing menuDescription plus a description that has become a long
model-facing prompt. The row pins the menuDescription, since §10's table is
operator autocomplete metadata. Its curated-out REASON was also factually
wrong ("interactive diagnostics UI" — it runs as a model turn now) and is
corrected; the false verdict survives on the invasive-config ground.

/fork gets the explicit written ChatSafe decision the record was missing:
false, on three independent grounds (transcript identity, worktree leakage,
unsupervised background session). It is genuinely enabled and visible in lab
sessions, so the verdict is load-bearing rather than moot. /subtask inherits
it — same spawn path.

Also records the standing catalog gap honestly: the pinned table is 33 rows
against a 105-row registry, a delta that is pre-existing (byte-identical at
2.1.220) and is now measured and curated in §10 as deliberately omitted,
with the add-candidates named. Widening the served composer surface is a
product decision, not a pin bump.

Fixes #235's work item 1 as a prerequisite: the two live-recipe assertions
still expected a resolved dialog to surface as a demoted tool chip, which
issue #56 changed to a dialog message carrying its recorded Outcome. They
failed identically on the old pin; nothing earlier in either test was
weakened.

nix/module.nix needs no edit — container.toolsImages derives its default tag
from versions.env at eval time and flake.nix prefix-greps it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

[autoland] verdict: pass
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!274
No description provided.