chore(agent-tools): bump the Claude Code pin 2.1.220 → 2.1.221 — Bash permission-check fix; compat re-verification after #235 [schedule: Daily agent update check] #273

Closed
opened 2026-08-04 04:12:16 +02:00 by dominik.polakovics · 1 comment

Goal

Move the agent-tools Claude Code pin from 2.1.220 to 2.1.221, re-verifying the compat record against the new binary first, exactly as containers/agent-tools/versions.env:11-16 and docs/ops.md § "Agent-tools images" mandate:

BEFORE bumping a version here, re-verify the compat record against the new CLI version FIRST (the compat doc is the checklist), THEN move the version + sha256 together. A bump that skips the re-verification ships an unverified CLI under a pin that claims verification.

The re-verification is the work. The two-line versions.env edit is the trivial part. If re-verification finds drift the record cannot absorb, the correct outcome is to report the drift on this issue and land only the compat-record updates — not to force the bump through.

Sequencing — read #235 first

#235 is a hard prerequisite and should land first (or in the same run). It covers the live tmux recipe suite that the 2.1.220 pin shipped without, plus two stale assertions in internal/compat/live_recipes_test.go that fail before the suite can tell you anything. Running this issue's re-verification on top of a live suite that has never been green on 2.1.220 gives you no baseline: you would not be able to tell 2.1.221 drift from the pre-existing #56 assertion bug.

Concretely: do #235's Work item 1 (fix the two assertions) and Work item 2 (green live run) first, then re-run the same suite against 2.1.221 and diff the outcomes.

#235 contains a claim that is now false and should not be trusted when you read it: "2.1.220 is still the newest release as of 2026-07-31 … There is nothing newer to chase; do not open the pin again as part of this." 2.1.221 published 2026-08-03. That statement was accurate when written and is superseded by this issue.

Evidence — current state

containers/agent-tools/versions.env:20-21:

CLAUDE_CODE_VERSION=2.1.220
CLAUDE_CODE_SHA256_X64_MUSL=f1c20514a3571cdf9982e25c490042d740ed7cfed3f00c64ba92dc7ec47c3c5b

internal/compat/compat.md:3 pins Claude Code 2.1.220, statically verified 2026-07-28 (bundle extraction + in-container CLI probes), live recipes still owed — that debt is #235.

Upstream, npm @anthropic-ai/claude-code dist-tags read on 2026-08-04:

$ curl -s https://registry.npmjs.org/@anthropic-ai/claude-code | jq '.["dist-tags"]'
{ "stable": "2.1.220", "latest": "2.1.221", "next": "2.1.221" }

publish times:  2.1.219  2026-07-24T16:11:49Z
                2.1.220  2026-07-24T23:11:21Z
                2.1.221  2026-08-03T22:16:25Z

Note the channel shape: our current pin 2.1.220 is now the stable channel, and 2.1.221 is latest/next. The 2.1.220 bump (#235, commit 52a9176) took latest over stable for a stated reason — 2.1.220 was the first release whose embedded catalog carries aliases:{opus:{default:"claude-opus-5"}}. This bump needs its own reason; the security item below is it. If the implementing agent concludes the reason is not strong enough, saying so and closing this issue is a legitimate outcome — but say it explicitly rather than silently deferring.

New pin values, from Anthropic's own per-version manifest (the source versions.env:17-19 names, and the exact artifact containers/agent-tools/build.sh:61-64 fetches):

$ curl -s https://downloads.claude.ai/claude-code-releases/2.1.221/manifest.json
  "version": "2.1.221",  "commit": "6efaf12e8b43dc7dbe50e0955c76dc4174a15876",
  "buildDate": "2026-08-03T21:08:11Z",
  "linux-x64-musl": { "checksum":
      "15b068e06eafff9b64583b46cdc065ac18b0d0d13950c2a83c6ee854f301a32f",
      "size": 283312720 }
CLAUDE_CODE_VERSION=2.1.221
CLAUDE_CODE_SHA256_X64_MUSL=15b068e06eafff9b64583b46cdc065ac18b0d0d13950c2a83c6ee854f301a32f

Re-derive that digest yourself from the downloaded binary (build.sh's fetch_artifact verifies it, so a wrong value fails the build loudly rather than silently) — do not trust this issue as the sole source.

Security — one item, and its reachability is conditional

2.1.221's changelog carries two permission-check fixes. Only one can reach lab, and whether it does depends on the operator's dev image:

  • Fixed a Bash tool permission-check bypass where zsh could execute hidden commands in [[ ]] regex conditionals; affected commands now prompt for permission
  • Fixed PowerShell permission checks mishandling paths containing quote characters on Windows; such paths now prompt for approval

The PowerShell one does not reach us — lab spawns Linux containers only.

The zsh one reaches us conditionally, and this is the honest framing:

  • Lab spawns claude with --permission-mode auto (internal/provider/claudecode/claudecode.go:419, argv documented at :392). Auto mode is exactly the mode whose classifier this bypass evades, and lab's whole model is unattended AFK runs — the permission check is the guard, and nobody is watching.
  • But the shell is not lab's to choose. grep -rn zsh over this repo returns zero hits. The agent-tools image (containers/agent-tools/Containerfile.claude) is FROM scratch and ships the claude binary plus labctl — no shell at all. The shell comes from the operator-supplied dev image (--container-image, internal/config/config.go:174; resolution at internal/instance/container.go:58-80), which lab does not pin.
  • So: on a bash-only dev image the specific bypass is not reachable. On a dev image that ships zsh as the session shell, it is. Since lab cannot know which, treat this as defense-in-depth that is worth taking rather than as an incident.
  • Blast radius even when reachable is one container instance and its mounted worktree — real, but bounded.

Deliberately not counted as a reason, stated so nobody re-derives it:

Fixed a rare wake-from-sleep race where two Claude Code processes could both refresh the same MCP connector or WIF OAuth token at once, forcing re-authentication

This reads like it belongs to lab's credential-authority seam (issue #222, internal/provider/claudecode/credauthority.go) — the single-refresher design that exists precisely because concurrent Claude Code processes refreshing one token family log each other out. It is the same class of bug, but a different token: the fix covers MCP connector and WIF OAuth tokens, and lab authenticates with authMethod:"claude.ai" (compat.md §3, M3 live acceptance). The seam's .credentials.json family is untouched by this fix. Do not treat it as closing any part of #222.

What changed that touches how this repo uses Claude Code

Every item below maps to a numbered section of internal/compat/compat.md. Each is a hypothesis to verify against a real 2.1.221 binary, not a confirmed break — none of it was observed live during this investigation.

§10 Builtin slash-command catalog (internal/provider/claudecode/commands.go, pinned by TestCompat_BuiltinCommands_pinned at internal/compat/compat_test.go:877) — the highest-confidence drift.

  • 2.1.221: "Changed /status to show the session kind: interactive, or a background job that is attached or unattended." /status is a chat-safe row (commands.go:49) and §10 pins builtin descriptions verbatim ("descriptions and argHints verbatim", compat.md:1042). Our row reads:
    "Show Claude Code status including version, model, account, API connectivity, and tool statuses"
    
    Re-extract it from the 2.1.221 bundle and confirm it still matches. A behavior change does not necessarily move the description string — but this is exactly what the verbatim pin is for.
  • Pre-existing catalog gap, worth closing in the same pass. The builtin table is a 2.1.198 scrape (compat.md:1034, "bundle extraction (2.1.198, 2026-07-08)"), 33 rows. The 2.1.220 static pass verified the ten chat-safe rows are still present — it did not check for rows added since. 2.1.221's changelog references /fork ("Changed sessions forked with /fork to create a new worktree of their own instead of working in the original session's checkout"), and grep -rn fork internal/provider/claudecode/ finds no such row. Do a fresh verbatim scrape at 2.1.221 and curate any new rows with the same rule §10 states (ChatSafe=true iff the command executes inline and returns to the prompt).
  • /fork in particular deserves an explicit ChatSafe decision with a written reason: every lab instance already is a linked git worktree, so a command that creates a worktree of its own inside one is at best confusing and at worst fights lab's own worktree management — the §10 curation rule's second clause.

§5/§8 Transcript + reply/interrupt recipes — lab's interrupt recipe is a single Escape, and the transcript JSONL is lab's entire chat surface.

  • 2.1.221: "Fixed @-mentioned files being silently dropped when pressing Esc to retract a prompt and resubmitting it." This changes what Esc-retract does to a composed-but-unsent prompt. Lab sends Escape as its interrupt; confirm the interrupt recipe still behaves as §8 pins it, and that a retract-then-resubmit does not now produce a transcript shape ParseTranscript has not seen.
  • 2.1.221: "Fixed CLAUDE_CODE_RESUME_INTERRUPTED_TURN=0 not disabling interrupted-turn auto-resume; falsy values are now honored." Lab does not set this variable (grep -rn CLAUDE_CODE_RESUME internal/ → no hits), so the default path is unchanged — but the note belongs in §8 next to the interrupt recipe, since interrupted-turn auto-resume is what lab's interrupt produces.
  • 2.1.221: "Fixed the thinking toggle having no effect for the rest of a session that started with thinking off." Lab pins --effort per run (claudecode.go:419); confirm the effort flag still drives what §1 says it does.

§1 Spawn argv{claude} [--remote-control <session>] --permission-mode auto [--model M] [--effort E] [prompt]

  • No flag rename in 2.1.221. The --permission-mode defaultmanual rename noted on #235 is a 2.1.220-era item and lab pins auto, which is unaffected — but fold it into §1 while you are there if #235 has not.
  • 2.1.221 adds no new spawn flag lab needs. --forward-subagent-text (2.1.219) remains unused.
  • Confirm claude --version reports 2.1.221 and that the embedded model catalog still resolves opus[1m] → Opus 5, the reason the 2.1.220 pin was taken.

§3/§3a Auth + config dir — unchanged by 2.1.221 as far as the changelog shows. Re-run the CLI probes anyway (claude auth status --json, CLAUDE_CONFIG_DIR honored); they are cheap and they are what §3 pins.

Not applicable, listed so the next sweep does not re-read them: the VSCode Focus view, Vim-mode yank/undo fixes, Windows startup and Bedrock/AWS-SSO fixes, /plugin and marketplace changes, emoji shortcodes, Claude-in-Chrome tab handling, Gateway model validation, Stats-panel token accounting, and the /ultrareview error-message changes. None of them touch a pinned coupling.

Scope — files an implementing agent will touch

Path Change
containers/agent-tools/versions.env CLAUDE_CODE_VERSION + CLAUDE_CODE_SHA256_X64_MUSL, moved atomically with the compat-record update
internal/compat/compat.md Pin header → 2.1.221; per-section provenance re-dated to what was actually re-verified; every drift above recorded in its own section
internal/provider/claudecode/commands.go Only if the §10 scrape shows drift (expected: new rows since 2.1.198; possible /status description change)
internal/compat/compat_test.go Only if TestCompat_BuiltinCommands_pinned needs a new pinned string — and only with the bundle evidence quoted in the commit message
internal/provider/claudecode/*.go + testdata/ Only if re-verification finds real drift. No port change is expected; if you find yourself rewriting the adapter, stop and report here instead

nix/module.nix needs no edit — container.toolsImages derives its default tag from versions.env at eval time (nix/module.nix:197-213), and flake.nix:320-322 prefix-greps the tag so a version bump never touches that check.

Done means

  • #235's live suite is green on 2.1.220 first, so 2.1.221 has a baseline to diff against.
  • containers/agent-tools/versions.env reads CLAUDE_CODE_VERSION=2.1.221 with the sha256 re-derived from the downloaded binary, and containers/agent-tools/build.sh claude builds the image with that digest verifying.
  • The agent-tools smoke test passes against the new image: containers/agent-tools/smoke-test.sh (driven the way .forgejo/workflows/agent-tools.yml drives it).
  • go test ./internal/compat/ ./internal/provider/claudecode/ is green — in particular TestCompat_BuiltinCommands_pinned.
  • The live suite executed (not skipped) and passed against a real 2.1.221 binary: LAB_COMPAT_LIVE=1 go test ./internal/compat/ -run Live -v. A skipped run is not a pass — the tests carry their own skip-gates and will quietly skip without tmux plus a logged-in claude on PATH; confirm from the -v output that they ran.
  • §10 re-scraped at 2.1.221: /status's description either confirmed byte-identical or updated with the new bundle text quoted in the commit message; any builtin added since 2.1.198 either added to the table with a written ChatSafe reason, or explicitly recorded in §10 as deliberately omitted.
  • /fork has an explicit, written ChatSafe decision.
  • internal/compat/compat.md:3 reads 2.1.221 with per-section provenance re-dated to what was actually re-verified — static markers marked static, live recipes marked live, nothing claimed that was not observed.
  • The zsh Bash-permission-bypass fix is recorded in compat.md next to §1's --permission-mode auto, with the conditional-reachability reasoning (operator-supplied dev image) so it is not re-derived next sweep.
  • go build -tags ui ./cmd/... && go test -tags ui ./... and golangci-lint run green (make build / make test / make lint).
  • PR body says Closes #273.

Out of scope

  • The codex pin — its own issue, #249 (target 0.146.0, still the newest stable as of 2026-08-04).
  • Go module and web/npm dependencies — #250.
  • Any adapter/port redesign. This is a pin bump plus the compat re-verification it requires; if the record cannot absorb what you find, report it here instead of expanding.

Filed by the Daily agent update check schedule, 2026-08-04. Version data from registry.npmjs.org/@anthropic-ai/claude-code; digest from downloads.claude.ai/claude-code-releases/2.1.221/manifest.json; changelog from github.com/anthropics/claude-code CHANGELOG.md. Nothing below §"What changed" was observed on a live 2.1.221 binary — every item is a hypothesis for the implementing agent to confirm or refute.

## Goal Move the agent-tools **Claude Code** pin from `2.1.220` to **`2.1.221`**, re-verifying the compat record against the new binary first, exactly as `containers/agent-tools/versions.env:11-16` and `docs/ops.md § "Agent-tools images"` mandate: > BEFORE bumping a version here, re-verify the compat record against the new CLI version FIRST (the compat doc is the checklist), THEN move the version + sha256 together. A bump that skips the re-verification ships an unverified CLI under a pin that claims verification. **The re-verification is the work. The two-line `versions.env` edit is the trivial part.** If re-verification finds drift the record cannot absorb, the correct outcome is to report the drift on this issue and land only the compat-record updates — not to force the bump through. ## Sequencing — read #235 first **#235 is a hard prerequisite and should land first (or in the same run).** It covers the live tmux recipe suite that the 2.1.220 pin shipped *without*, plus two stale assertions in `internal/compat/live_recipes_test.go` that fail before the suite can tell you anything. Running this issue's re-verification on top of a live suite that has never been green on 2.1.220 gives you no baseline: you would not be able to tell 2.1.221 drift from the pre-existing #56 assertion bug. Concretely: do #235's Work item 1 (fix the two assertions) and Work item 2 (green live run) first, *then* re-run the same suite against 2.1.221 and diff the outcomes. **#235 contains a claim that is now false** and should not be trusted when you read it: *"**2.1.220 is still the newest release** as of 2026-07-31 … There is nothing newer to chase; do not open the pin again as part of this."* 2.1.221 published 2026-08-03. That statement was accurate when written and is superseded by this issue. ## Evidence — current state `containers/agent-tools/versions.env:20-21`: ``` CLAUDE_CODE_VERSION=2.1.220 CLAUDE_CODE_SHA256_X64_MUSL=f1c20514a3571cdf9982e25c490042d740ed7cfed3f00c64ba92dc7ec47c3c5b ``` `internal/compat/compat.md:3` pins **Claude Code 2.1.220**, statically verified 2026-07-28 (bundle extraction + in-container CLI probes), live recipes still owed — that debt is #235. Upstream, npm `@anthropic-ai/claude-code` dist-tags read on 2026-08-04: ``` $ curl -s https://registry.npmjs.org/@anthropic-ai/claude-code | jq '.["dist-tags"]' { "stable": "2.1.220", "latest": "2.1.221", "next": "2.1.221" } publish times: 2.1.219 2026-07-24T16:11:49Z 2.1.220 2026-07-24T23:11:21Z 2.1.221 2026-08-03T22:16:25Z ``` Note the channel shape: **our current pin `2.1.220` is now the `stable` channel**, and `2.1.221` is `latest`/`next`. The 2.1.220 bump (#235, commit `52a9176`) took `latest` over `stable` for a stated reason — 2.1.220 was the first release whose embedded catalog carries `aliases:{opus:{default:"claude-opus-5"}}`. **This bump needs its own reason**; the security item below is it. If the implementing agent concludes the reason is not strong enough, saying so and closing this issue is a legitimate outcome — but say it explicitly rather than silently deferring. New pin values, from Anthropic's own per-version manifest (the source `versions.env:17-19` names, and the exact artifact `containers/agent-tools/build.sh:61-64` fetches): ``` $ curl -s https://downloads.claude.ai/claude-code-releases/2.1.221/manifest.json "version": "2.1.221", "commit": "6efaf12e8b43dc7dbe50e0955c76dc4174a15876", "buildDate": "2026-08-03T21:08:11Z", "linux-x64-musl": { "checksum": "15b068e06eafff9b64583b46cdc065ac18b0d0d13950c2a83c6ee854f301a32f", "size": 283312720 } ``` ``` CLAUDE_CODE_VERSION=2.1.221 CLAUDE_CODE_SHA256_X64_MUSL=15b068e06eafff9b64583b46cdc065ac18b0d0d13950c2a83c6ee854f301a32f ``` **Re-derive that digest yourself** from the downloaded binary (`build.sh`'s `fetch_artifact` verifies it, so a wrong value fails the build loudly rather than silently) — do not trust this issue as the sole source. ## Security — one item, and its reachability is conditional `2.1.221`'s changelog carries two permission-check fixes. Only one can reach lab, and whether it does depends on the operator's dev image: > - Fixed a **Bash tool permission-check bypass where zsh could execute hidden commands in `[[ ]]` regex conditionals**; affected commands now prompt for permission > - Fixed PowerShell permission checks mishandling paths containing quote characters on Windows; such paths now prompt for approval **The PowerShell one does not reach us** — lab spawns Linux containers only. **The zsh one reaches us conditionally, and this is the honest framing:** - Lab spawns `claude` with `--permission-mode auto` (`internal/provider/claudecode/claudecode.go:419`, argv documented at `:392`). Auto mode is exactly the mode whose classifier this bypass evades, and lab's whole model is *unattended* AFK runs — the permission check is the guard, and nobody is watching. - **But the shell is not lab's to choose.** `grep -rn zsh` over this repo returns **zero** hits. The agent-tools image (`containers/agent-tools/Containerfile.claude`) is `FROM scratch` and ships the `claude` binary plus `labctl` — no shell at all. The shell comes from the **operator-supplied dev image** (`--container-image`, `internal/config/config.go:174`; resolution at `internal/instance/container.go:58-80`), which lab does not pin. - So: on a bash-only dev image the specific bypass is **not** reachable. On a dev image that ships zsh as the session shell, it **is**. Since lab cannot know which, treat this as defense-in-depth that is worth taking rather than as an incident. - Blast radius even when reachable is one container instance and its mounted worktree — real, but bounded. **Deliberately not counted as a reason, stated so nobody re-derives it:** > Fixed a rare wake-from-sleep race where two Claude Code processes could both refresh the same MCP connector or WIF OAuth token at once, forcing re-authentication This reads like it belongs to lab's credential-authority seam (issue #222, `internal/provider/claudecode/credauthority.go`) — the single-refresher design that exists precisely because concurrent Claude Code processes refreshing one token family log each other out. It is the **same class of bug**, but a different token: the fix covers *MCP connector* and *WIF OAuth* tokens, and lab authenticates with `authMethod:"claude.ai"` (`compat.md` §3, M3 live acceptance). The seam's `.credentials.json` family is untouched by this fix. Do not treat it as closing any part of #222. ## What changed that touches how this repo uses Claude Code Every item below maps to a numbered section of `internal/compat/compat.md`. Each is a **hypothesis to verify against a real 2.1.221 binary**, not a confirmed break — none of it was observed live during this investigation. **§10 Builtin slash-command catalog** (`internal/provider/claudecode/commands.go`, pinned by `TestCompat_BuiltinCommands_pinned` at `internal/compat/compat_test.go:877`) — **the highest-confidence drift.** - 2.1.221: *"Changed `/status` to show the session kind: `interactive`, or a background job that is `attached` or `unattended`."* `/status` is a **chat-safe** row (`commands.go:49`) and §10 pins builtin descriptions **verbatim** ("descriptions and argHints **verbatim**", `compat.md:1042`). Our row reads: ``` "Show Claude Code status including version, model, account, API connectivity, and tool statuses" ``` Re-extract it from the 2.1.221 bundle and confirm it still matches. A behavior change does not necessarily move the description string — but this is exactly what the verbatim pin is for. - **Pre-existing catalog gap, worth closing in the same pass.** The builtin table is a **2.1.198** scrape (`compat.md:1034`, "bundle extraction (2.1.198, 2026-07-08)"), 33 rows. The 2.1.220 static pass verified the ten chat-safe rows are *still present* — it did **not** check for rows *added* since. 2.1.221's changelog references `/fork` (*"Changed sessions forked with `/fork` to create a new worktree of their own instead of working in the original session's checkout"*), and `grep -rn fork internal/provider/claudecode/` finds no such row. Do a fresh verbatim scrape at 2.1.221 and curate any new rows with the same rule §10 states (`ChatSafe=true` iff the command executes inline and returns to the prompt). - `/fork` in particular deserves an explicit `ChatSafe` decision with a written reason: every lab instance already *is* a linked git worktree, so a command that creates a worktree of its own inside one is at best confusing and at worst fights lab's own worktree management — the §10 curation rule's second clause. **§5/§8 Transcript + reply/interrupt recipes** — lab's interrupt recipe is a single Escape, and the transcript JSONL is lab's entire chat surface. - 2.1.221: *"Fixed @-mentioned files being silently dropped when pressing **Esc** to retract a prompt and resubmitting it."* This changes what Esc-retract does to a composed-but-unsent prompt. Lab sends Escape as its interrupt; confirm the interrupt recipe still behaves as §8 pins it, and that a retract-then-resubmit does not now produce a transcript shape `ParseTranscript` has not seen. - 2.1.221: *"Fixed `CLAUDE_CODE_RESUME_INTERRUPTED_TURN=0` not disabling interrupted-turn auto-resume; falsy values are now honored."* Lab does **not** set this variable (`grep -rn CLAUDE_CODE_RESUME internal/` → no hits), so the default path is unchanged — but the note belongs in §8 next to the interrupt recipe, since interrupted-turn auto-resume is what lab's interrupt produces. - 2.1.221: *"Fixed the thinking toggle having no effect for the rest of a session that started with thinking off."* Lab pins `--effort` per run (`claudecode.go:419`); confirm the effort flag still drives what §1 says it does. **§1 Spawn argv** — `{claude} [--remote-control <session>] --permission-mode auto [--model M] [--effort E] [prompt]` - No flag rename in 2.1.221. The `--permission-mode` `default` → `manual` rename noted on #235 is a 2.1.220-era item and lab pins `auto`, which is unaffected — but fold it into §1 while you are there if #235 has not. - 2.1.221 adds no new spawn flag lab needs. `--forward-subagent-text` (2.1.219) remains unused. - Confirm `claude --version` reports `2.1.221` and that the embedded model catalog still resolves `opus[1m]` → Opus 5, the reason the 2.1.220 pin was taken. **§3/§3a Auth + config dir** — unchanged by 2.1.221 as far as the changelog shows. Re-run the CLI probes anyway (`claude auth status --json`, `CLAUDE_CONFIG_DIR` honored); they are cheap and they are what §3 pins. **Not applicable, listed so the next sweep does not re-read them:** the VSCode Focus view, Vim-mode yank/undo fixes, Windows startup and Bedrock/AWS-SSO fixes, `/plugin` and marketplace changes, emoji shortcodes, Claude-in-Chrome tab handling, Gateway `model` validation, Stats-panel token accounting, and the `/ultrareview` error-message changes. None of them touch a pinned coupling. ## Scope — files an implementing agent will touch | Path | Change | |---|---| | `containers/agent-tools/versions.env` | `CLAUDE_CODE_VERSION` + `CLAUDE_CODE_SHA256_X64_MUSL`, moved **atomically with the compat-record update** | | `internal/compat/compat.md` | Pin header → 2.1.221; per-section provenance re-dated to what was actually re-verified; every drift above recorded in its own section | | `internal/provider/claudecode/commands.go` | Only if the §10 scrape shows drift (expected: new rows since 2.1.198; possible `/status` description change) | | `internal/compat/compat_test.go` | Only if `TestCompat_BuiltinCommands_pinned` needs a new pinned string — and only with the bundle evidence quoted in the commit message | | `internal/provider/claudecode/*.go` + `testdata/` | Only if re-verification finds real drift. **No port change is expected**; if you find yourself rewriting the adapter, stop and report here instead | `nix/module.nix` needs **no** edit — `container.toolsImages` derives its default tag from `versions.env` at eval time (`nix/module.nix:197-213`), and `flake.nix:320-322` prefix-greps the tag so a version bump never touches that check. ## Done means - [ ] #235's live suite is green on **2.1.220** first, so 2.1.221 has a baseline to diff against. - [ ] `containers/agent-tools/versions.env` reads `CLAUDE_CODE_VERSION=2.1.221` with the sha256 **re-derived from the downloaded binary**, and `containers/agent-tools/build.sh claude` builds the image with that digest verifying. - [ ] The agent-tools smoke test passes against the new image: `containers/agent-tools/smoke-test.sh` (driven the way `.forgejo/workflows/agent-tools.yml` drives it). - [ ] `go test ./internal/compat/ ./internal/provider/claudecode/` is green — in particular `TestCompat_BuiltinCommands_pinned`. - [ ] The live suite executed (not skipped) and passed against a real 2.1.221 binary: `LAB_COMPAT_LIVE=1 go test ./internal/compat/ -run Live -v`. **A skipped run is not a pass** — the tests carry their own skip-gates and will quietly skip without tmux plus a logged-in `claude` on PATH; confirm from the `-v` output that they ran. - [ ] §10 re-scraped at 2.1.221: `/status`'s description either confirmed byte-identical or updated with the new bundle text quoted in the commit message; any builtin added since 2.1.198 either added to the table with a written `ChatSafe` reason, or explicitly recorded in §10 as deliberately omitted. - [ ] `/fork` has an explicit, written `ChatSafe` decision. - [ ] `internal/compat/compat.md:3` reads **2.1.221** with per-section provenance re-dated to what was actually re-verified — static markers marked static, live recipes marked live, nothing claimed that was not observed. - [ ] The zsh Bash-permission-bypass fix is recorded in `compat.md` next to §1's `--permission-mode auto`, with the conditional-reachability reasoning (operator-supplied dev image) so it is not re-derived next sweep. - [ ] `go build -tags ui ./cmd/... && go test -tags ui ./...` and `golangci-lint run` green (`make build` / `make test` / `make lint`). - [ ] PR body says `Closes #273`. ## Out of scope - The codex pin — its own issue, #249 (target `0.146.0`, still the newest stable as of 2026-08-04). - Go module and web/npm dependencies — #250. - Any adapter/port redesign. This is a pin bump plus the compat re-verification it requires; if the record cannot absorb what you find, report it here instead of expanding. --- *Filed by the Daily agent update check schedule, 2026-08-04. Version data from `registry.npmjs.org/@anthropic-ai/claude-code`; digest from `downloads.claude.ai/claude-code-releases/2.1.221/manifest.json`; changelog from `github.com/anthropics/claude-code` `CHANGELOG.md`. Nothing below §"What changed" was observed on a live 2.1.221 binary — every item is a hypothesis for the implementing agent to confirm or refute.*
Author
Owner

PR #274 opened. Summary of what the re-verification actually found, including the two things worth a maintainer's attention.

The live gate is closed, with a real baseline. #235's work item 1 (the two stale ReadChat assertions) and work item 2 (the green live run) are both in this PR, because the issue was right that 2.1.221 is unreadable without them. The suite ran against the outgoing 2.1.220 binary first, then 2.1.221: six tests, zero skips, identical verdicts on both. That is what makes "green" mean "no drift" here rather than "green for some other reason".

Confirmed against a real binary (every hypothesis in the issue was checked, not assumed):

  • /statusbyte-identical. The session-kind behavior changed; the description string did not. This was the highest-confidence predicted drift and it did not happen.
  • §1 flags — no drift. The entire --help diff 2.1.220 → 2.1.221 is one added line, --autocompact <auto|tokens>, which lab does not use. --permission-mode still takes auto; --effort values unchanged.
  • The opusclaude-opus-5 catalog alias — intact, so the reason the 2.1.220 pin was taken survives.
  • §3/§3a — same JSON key set, CLAUDE_CONFIG_DIR still honored.
  • The digest was re-derived from the downloaded bytes as instructed, and the musl artifact was then executed through Alpine's musl loader (the same proof the Containerfile's pre-patch sanity run makes).

Two findings you did not ask for, but should see:

  1. Three pinned descriptions were already stale — on 2.1.220, not 2.1.221. feedback, usage-credits and doctor no longer carry their 2.1.198 text. The 2.1.220 pass re-verified only the ten chat-safe rows, so it never looked at the curated-out ones. All three are ChatSafe=false, so nothing chat-facing ever served wrong text, and TestCompat_BuiltinCommands_pinned could not have caught it. Fixed with bundle evidence in the commit message.

    /doctor is the interesting one: it is now a prompt-skill that runs as a model turn, carrying an operator-facing menuDescription plus a long model-facing description. §10's stated reason for excluding it ("interactive diagnostics UI") was therefore factually wrong and would have argued for ChatSafe=true. The false verdict survives, but on a different ground (it mutates operator-global and project config), and the reason is corrected in the record.

  2. A method bug that would have hidden this. A forward regex over description:"…" reports a renamed key as an absent row — which is exactly how /doctor was missed. The check that settles drift is the reverse direction: take each pinned string and grep the raw binary. §10 now says so, along with the fact that the documented tr -c '[:print:]' dump silently drops strings inside long printable runs.

/fork got the explicit written decision you asked for: ChatSafe=false. Worth noting it is genuinely enabled and visible in every lab session (its gate needs CLAUDE_CODE_COORDINATOR_MODE, which lab does not set), so the verdict is load-bearing rather than moot. Three grounds: transcript identity, worktree leakage, unsupervised background session. /subtask inherits it.

The catalog gap is now measured rather than vague: 33 pinned rows against a 105-name registry. That 72-row delta is pre-existing — byte-identical at 2.1.220 — so it accumulated between 2.1.198 and 2.1.220. It is recorded in §10 as deliberately omitted with the add-candidates named (help is the strongest). I did not widen the served table: that changes what every operator sees in the composer, which reads like a product decision wanting its own issue rather than something to slip into a pin bump. Say the word if you would rather it went in here.

One gap, stated plainly. The container build and injection smoke test did not run locally — this host has no container runtime and no root, so build.sh and smoke-test.sh could not execute. Mitigations: the digest verifies under build.sh's exact sha256sum -c check, the artifact was executed through the loader the image bundles, and the PR is path-gated onto the agent-tools CI workflow (versions.env is under containers/**), whose smoke job builds both images and runs the injection test against debian:stable-slim and alpine. That CI leg is the authority on the image — please confirm it is green before merging.

Also not re-driven, and therefore claimed nowhere 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.

PR #274 opened. Summary of what the re-verification actually found, including the two things worth a maintainer's attention. **The live gate is closed, with a real baseline.** #235's work item 1 (the two stale `ReadChat` assertions) and work item 2 (the green live run) are both in this PR, because the issue was right that 2.1.221 is unreadable without them. The suite ran against the **outgoing 2.1.220 binary first**, then 2.1.221: six tests, **zero skips**, identical verdicts on both. That is what makes "green" mean "no drift" here rather than "green for some other reason". **Confirmed against a real binary** (every hypothesis in the issue was checked, not assumed): - `/status` — **byte-identical**. The session-kind behavior changed; the description string did not. This was the highest-confidence predicted drift and it did not happen. - §1 flags — no drift. The entire `--help` diff 2.1.220 → 2.1.221 is **one added line**, `--autocompact <auto|tokens>`, which lab does not use. `--permission-mode` still takes `auto`; `--effort` values unchanged. - The `opus` → `claude-opus-5` catalog alias — intact, so the reason the 2.1.220 pin was taken survives. - §3/§3a — same JSON key set, `CLAUDE_CONFIG_DIR` still honored. - The digest was **re-derived from the downloaded bytes** as instructed, and the musl artifact was then *executed* through Alpine's musl loader (the same proof the Containerfile's pre-patch sanity run makes). **Two findings you did not ask for, but should see:** 1. **Three pinned descriptions were already stale — on 2.1.220, not 2.1.221.** `feedback`, `usage-credits` and `doctor` no longer carry their 2.1.198 text. The 2.1.220 pass re-verified only the *ten chat-safe rows*, so it never looked at the curated-out ones. All three are ChatSafe=false, so nothing chat-facing ever served wrong text, and `TestCompat_BuiltinCommands_pinned` could not have caught it. Fixed with bundle evidence in the commit message. `/doctor` is the interesting one: it is now a prompt-skill that **runs as a model turn**, carrying an operator-facing `menuDescription` plus a long model-facing `description`. §10's stated reason for excluding it ("interactive diagnostics UI") was therefore factually wrong and would have argued for ChatSafe=true. The `false` verdict survives, but on a different ground (it mutates operator-global and project config), and the reason is corrected in the record. 2. **A method bug that would have hidden this.** A forward regex over `description:"…"` reports a **renamed key as an absent row** — which is exactly how `/doctor` was missed. The check that settles drift is the reverse direction: take each pinned string and grep the raw binary. §10 now says so, along with the fact that the documented `tr -c '[:print:]'` dump silently drops strings inside long printable runs. **`/fork`** got the explicit written decision you asked for: **ChatSafe=false**. Worth noting it is genuinely *enabled and visible* in every lab session (its gate needs `CLAUDE_CODE_COORDINATOR_MODE`, which lab does not set), so the verdict is load-bearing rather than moot. Three grounds: transcript identity, worktree leakage, unsupervised background session. `/subtask` inherits it. **The catalog gap is now measured rather than vague:** 33 pinned rows against a 105-name registry. That 72-row delta is **pre-existing** — byte-identical at 2.1.220 — so it accumulated between 2.1.198 and 2.1.220. It is recorded in §10 as deliberately omitted with the add-candidates named (`help` is the strongest). I did not widen the served table: that changes what every operator sees in the composer, which reads like a product decision wanting its own issue rather than something to slip into a pin bump. Say the word if you would rather it went in here. **One gap, stated plainly.** The container build and injection smoke test did **not** run locally — this host has no container runtime and no root, so `build.sh` and `smoke-test.sh` could not execute. Mitigations: the digest verifies under `build.sh`'s exact `sha256sum -c` check, the artifact was executed through the loader the image bundles, and the PR is path-gated onto the `agent-tools` CI workflow (`versions.env` is under `containers/**`), whose `smoke` job builds both images and runs the injection test against `debian:stable-slim` and `alpine`. **That CI leg is the authority on the image — please confirm it is green before merging.** Also not re-driven, and therefore claimed nowhere 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.
Sign in to join this conversation.
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#273
No description provided.