### Preflight Checklist
- [x] I have searched [existing issues](https://github.…com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
### What's Wrong?
```
The Windows native updater decides whether the installed launcher already matches
the new build by comparing FILE SIZE ONLY. When two consecutive releases happen to
compile to the exact same byte size, it concludes the launcher is already up to date,
skips the copy entirely, and returns a result the rest of the update pipeline cannot
distinguish from a real success.
The result: `claude update` prints "Successfully updated from 2.1.225 to version 2.1.226",
writes `"outcome":"success"` with `"error_code":null` to ~/.claude/.last-update-result.json,
and leaves the launcher completely untouched. `claude --version` still reports the old
version. Every subsequent `claude update` repeats the identical false success forever.
This hit the 2.1.225 -> 2.1.226 pair, which are byte-identical in size:
2.1.223 280,233,632 bytes
2.1.224 284,981,920 bytes
2.1.225 287,053,472 bytes
2.1.226 287,053,472 bytes <-- same size as 2.1.225, different binary
MD5 2.1.225 f7ad3f8bb03a3f54f96910126c91191e
MD5 2.1.226 04df2e354071833ad36016e62cec7639
The staged build in ~/.local/share/claude/versions/2.1.226 was downloaded correctly.
Only the activation step (copying it over ~/.local/bin/claude.exe) was skipped.
The false success is the worse half of this bug. A visible failure would be a minor
annoyance; reporting success while doing nothing defeats every diagnostic path at once —
the user-facing message, the update-result record, and the telemetry event
(tengu_native_update_complete / tengu_native_auto_updater_success) all say it worked.
These failures are therefore almost certainly being counted as successful updates in
your metrics, so the true failure rate is invisible.
Note: `claude update --force` does NOT help — the force flag controls download/reinstall,
not the activation step where the size check lives. `claude doctor` does not surface it either.
```
### What Should Happen?
```
`claude update` should either actually replace the launcher with the new build, or
report a failure. It should never report success while leaving the old binary in place.
Specifically:
1. Equal file size must not be treated as proof of equal content. Compare a content
hash, or compare the launcher's reported version against the target version, or
simply always perform the swap.
2. After activation, verify the launcher actually resolves to the target version and
fail loudly if it does not. This single check would have caught the bug regardless
of its cause, and would catch any future activation failure too.
3. The skip path should emit its own telemetry outcome instead of being folded into
the success count.
```
### Error Messages/Logs
```shell
There is no error — that is the bug. For completeness:
Terminal output of the failed update:
Current version: 2.1.225
Checking for updates to latest version...
Updating to 2.1.226...
Successfully updated from 2.1.225 to version 2.1.226
Immediately afterwards:
> claude --version
2.1.225 (Claude Code)
~/.claude/.last-update-result.json written by that run:
{"timestamp":"2026-08-09T11:58:51.050Z","path":"native","outcome":"success",
"status":"success","version_from":"2.1.225","version_to":"2.1.226","error_code":null}
```
### Steps to Reproduce
```
1. On Windows with a native install, be on version A.
2. Have version B released where sizeof(B) == sizeof(A) but B != A.
(Real-world instance: A = 2.1.225, B = 2.1.226, both exactly 287,053,472 bytes.)
3. Run `claude update`.
-> It prints "Successfully updated from A to version B".
4. Run `claude --version`.
-> Still reports A.
5. Confirm ~/.local/share/claude/versions/B exists and is the correct new binary,
while ~/.local/bin/claude.exe still hashes identical to versions/A.
6. Repeat step 3 any number of times — identical false success every time.
Root cause, from the Windows branch of the launcher-activation function in the
shipped 2.1.226 binary (minified):
async function fTS(e, t) {
if (Sxe().startsWith("win32")) try {
let s = aS.dirname(e);
await gf.mkdir(s, { recursive: !0 });
let a;
try { a = await gf.stat(e) } catch {}
if (a) {
try {
let c = await gf.stat(t);
if (a.size === c.size) return "noop"; // <-- BUG
} catch {}
let l = `${e}.old.${Date.now()}`;
await gf.rename(e, l);
try {
await gf.copyFile(t, e);
try { await gf.unlink(l) } catch {}
} catch (c) { /* restore */ }
}
...
return "updated"
} catch (s) { return "failed" }
`e` is the launcher path, `t` is the staged new build. `a.size === c.size` treats equal
file size as proof of equal content and returns "noop" before the rename-and-copy runs.
The caller cannot tell "noop" from success:
let l = await fTS(o, n);
if (l !== "updated" && !await VKt(o)) { ...throw... }
return { wasNewInstall: s, moveRetried: a,
activationFailed: l === "failed",
activationRefused: l === "refused" };
With l === "noop" the launcher still exists and is a valid executable, so VKt(o) is
truthy and nothing is thrown. activationFailed and activationRefused are both false.
The caller returns {success: true, latestVersion: l}, logs "Successfully updated to
version ...", which becomes wasUpdated: true and the "outcome":"success" record.
The surrounding code already distinguishes "failed" and "refused" outcomes and has a
rich error classifier including swap_failure and av_quarantine categories — the "noop"
path bypasses all of it.
```
### Claude Model
_No response_
### Is this a regression?
Yes, this worked in a previous version
### Last Working Version
2.1.224
### Claude Code Version
2.1.226
### Platform
Anthropic API
### Operating System
Windows
### Terminal/Shell
VS Code integrated terminal
### Additional Information
```
Antivirus ruled out
-------------------
Including this because AV is the natural first suspicion for a Windows file-replacement
failure. Kaspersky is installed on this machine. It is not involved:
1. No file operation is ever attempted. The function returns "noop" before the
rename/copy, so there is nothing for AV to block.
2. The same operations succeed manually. Renaming the RUNNING claude.exe and copying
the 287 MB staged build over it both completed without error, with Kaspersky active.
(Renaming a running executable is permitted here, so the exe-lock path is not
implicated either.)
3. The product's own AV detection never fired — error_code was null and the outcome
was "success", not "av_quarantine".
Is it really a regression?
--------------------------
Probably not in the code. The size-only shortcut appears in both 2.1.225 and 2.1.226,
and updates 2.1.223 -> 2.1.224 -> 2.1.225 all applied correctly because those builds
differed in size. So this looks like a long-standing latent bug that the 2.1.225/2.1.226
size collision triggered for the first time, rather than something newly introduced.
I answered the regression question as "yes" because updating did work for me previously,
but the underlying defect is likely older than 2.1.225.
Scope
-----
Any Windows native-install user whose installed version happens to match the byte size
of the next release will be silently pinned to their current version indefinitely, with
the CLI insisting the update succeeded. Since the size collision is a coincidence, the
affected population will look random and intermittent from the outside, which will make
it hard to correlate from telemetry — especially as these runs are recorded as successes.
Workaround for affected users
-----------------------------
Copying the staged build over the launcher by hand restores the correct version:
mv ~/.local/bin/claude.exe ~/.local/bin/claude.exe.bak
cp ~/.local/share/claude/versions/<target> ~/.local/bin/claude.exe
claude --version # verify
Alternatively, removing the launcher entirely before updating takes the else-branch
(copyFile with no size check) and applies correctly.
```