Skip to content

fix(system-update): report Claude Code update outcomes truthfully - #59

Merged
verlyn13 merged 1 commit into
mainfrom
fix/claude-code-update-reporting
Aug 1, 2026
Merged

fix(system-update): report Claude Code update outcomes truthfully#59
verlyn13 merged 1 commit into
mainfrom
fix/claude-code-update-reporting

Conversation

@verlyn13

@verlyn13 verlyn13 commented Aug 1, 2026

Copy link
Copy Markdown
Member

Problem

step_claude_code branched on the exit status of claude update, guided by a comment asserting it "exits nonzero when already at latest". It does not — it exits 0 for updated, already-latest, and failed alike. Both arms also returned 0, so the step was structurally incapable of reporting a failed update.

Evidence from the local run logs:

claude: successfully updated printed 72× across 111 logs
Actual version changes 3 (2.1.208→209, 214→215, 218→219)
Claude Code step ever classified non-ok never

summarize_step keyed its console detail off that same string, so an untouched install rendered as:

✓ Claude Code              1s  updated to 2.1.220

…and a genuine update failure would have rendered as a green check with no notice.

This was not a recent regression. git log -L shows step_claude_code had exactly one revision — the original 5881ed6.

Fix

Decide the outcome from what the updater reported, not from its exit status, and emit a claude-update-failed: marker with a nonzero return when it reports neither an update nor an up-to-date version.

  • is_network_failure — added failed to fetch version from, the signature of claude's release-feed failure (Failed to fetch version from https://downloads.claude.ai/... after 3 attempt(s)). No existing alternative matched it, so a transient fetch error would otherwise land in the hard fail lane instead of the retryable network one.
  • summarize_step — reports update did not apply on failure.
  • collect_notices — claims the failure so it stops reaching the residual catch-all, which could only describe it as an unexplained warning. The action item is suppressed when the shared network notice already covers it.

Validation

  • scripts/validate-repo.shPASS
  • tests/system-update/run.sh135 passed, 0 failed (9 new)

New tests run against a stub that reproduces the exit-0-on-failure behavior, covering updated / up-to-date / retryable-fetch / hard-fail, plus a guard that a clean run stays silent so the notice assertions can't pass for a check that always complains.

Live, against the real binary:

✓ Claude Code              1s  up to date (2.1.220)

Behavior change

Default-mode behavior is unchanged: only --strict aborts on a failed step, and a network-class failure still counts as a warning, so system_update_hard_failed (and the process exit contract) holds. Verified no automation invokes --strict and SYSTEM_UPDATE_NETWORK_STRICT is unset.

A genuine non-network Claude Code update failure now exits 1 where it previously reported success. That is the point of the change, but it is a real change for anything gating on the exit code.

Not in scope

The install_failed record in ~/.claude/.last-update-result.json (2026-07-31, version_to: null) comes from Claude Code's own in-process background auto-updater, a separate lane from system-update. It failed at the version-fetch stage despite the label, and that file is only written on an update attempt — so it stays stale while the CLI is current. Left alone; it self-corrects on the next attempt.

step_claude_code branched on the exit status of `claude update`, guided by a
comment asserting it "exits nonzero when already at latest". It does not: it
exits 0 for updated, already-latest, and failed alike. Both arms also returned
0, so the step was structurally incapable of reporting a failed update.

Across 111 run logs the step printed "claude: successfully updated" 72 times
against 3 real version changes, and summarize_step keyed its console detail off
that same string — so an untouched install rendered as "updated to 2.1.220",
and a genuine failure would have rendered as a green check.

Decide the outcome from what the updater reported instead of its exit status,
and emit a `claude-update-failed:` marker with a nonzero return when it reports
neither an update nor an up-to-date version.

- is_network_failure: add `failed to fetch version from`, the signature of
  claude's release-feed failure. No existing alternative matched it, so a
  transient fetch error would otherwise land in the hard fail lane instead of
  the retryable one.
- summarize_step: report "update did not apply" on failure.
- collect_notices: claim the failure so it stops reaching the residual
  catch-all, which could only describe it as an unexplained warning. The
  action item is suppressed when the shared network notice already covers it.
- tests: 9 assertions over a stub that reproduces the exit-0-on-failure
  behavior, covering updated / up-to-date / retryable / hard-fail, plus a
  guard that a clean run stays silent.

Default-mode behavior is unchanged: only --strict aborts on a failed step, and
a network-class failure still counts as a warning, so the process exit contract
holds. A genuine non-network update failure now exits 1 where it previously
reported success.
@verlyn13
verlyn13 requested a review from a team as a code owner August 1, 2026 23:13
@verlyn13
verlyn13 merged commit 5115c66 into main Aug 1, 2026
2 checks passed
@verlyn13
verlyn13 deleted the fix/claude-code-update-reporting branch August 1, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant