CLAUDE_CODE_LOCAL_BINARY override still dead code in Desktop 1.24012.11 (Linux) — initLocalBinary() has no call site; env var is read and discarded

Status Open
Reported on v2.1.219
Maintainer reply None cached
Activity 0 comments · opened Aug 6, 2026

Bug report: Claude Desktop reads CLAUDE_CODE_LOCAL_BINARY and discards it

Body for an issue on anthropics/claude-code. Everything below is reproducible from a
stock install; no patched files were involved in finding it.

Proposed title:

CLAUDE_CODE_LOCAL_BINARY override still dead code in Desktop 1.24012.11 (Linux) — initLocalBinary() has no call site; env var is read and discarded

---

Prior art — please read before auto-closing

This has been reported twice and is still present three months later.

  • #62720 (Linux, 2026-05-27) — the same defect. Closed 2026-06-28 by the stale bot

with *"Closing for now — inactive for too long. Please open a new issue if this is
still relevant."* It is still relevant; this is that new issue.

  • #59450 (Windows MSIX, 2026-05-15) — also the same defect, and well written. Closed

as a duplicate of #42203.

This is not a duplicate of #42203. That issue is *"Desktop app ignores env.PATH from
settings.json (sandbox overrides)"* — a different mechanism with a different cause: it
concerns the env block in settings.json not reaching the child's PATH. The defect
here is that CLAUDE_CODE_LOCAL_BINARY is present and correct in the child's
environment, and the app's own binary-resolution code contains no call site for the method
that would consume it. The two share only the words "Desktop app ignores env"; fixing
#42203 would not touch this code path, and fixing this would not touch #42203.

Summary: Claude Desktop's CLAUDE_CODE_LOCAL_BINARY override is dead-wired. The
value is read once and thrown away, and initLocalBinary() — the method that consumes it
— is defined but never called anywhere in the bundle. The app then propagates the variable
into the spawned session's environment, so every downstream check for "is the override
active?" reports yes while the app is in fact running its pinned build.

Affected: claude-desktop 1.24012.11 (Linux, apt downloads.claude.ai/claude-desktop/apt/stable),
bundling Claude Code 2.1.219. Bundle: /usr/lib/claude-desktop/resources/app.asar.

Severity: low impact, high diagnosability cost. Nothing crashes; the override simply
never engages, and it fails in the one direction that resists debugging — every symptom
points at the user's binary rather than at the app.

Where

In the CCD binary-resolution class, the constructor's final statement is a bare property
read with the result discarded:

_.info(`[CCD] Initialized with version ${this.buildPinVersion}`),
process.env.CLAUDE_CODE_LOCAL_BINARY   // ← value computed, never used
}
async initLocalBinary(t){
  try{ await W.access(t, Y.constants.X_OK),
       this.localBinaryPath = t,
       _.warn(`[CCD] LOCAL OVERRIDE: Using local binary at ${t}`) }
  catch{ _.error(`[CCD] LOCAL OVERRIDE: Binary not found or not executable at ${t}, falling back to normal flow`) }
}

It looks like the intended statement was an assignment guarded on the variable —
something of the shape process.env.X && (this.localBinaryInitPromise = this.initLocalBinary(process.env.X))
— and the assignment half was lost.

Consequences, in order:

  1. this.localBinaryInitPromise is initialised to null and never assigned again.
  2. getLocalBinaryPath() awaits that null and returns this.localBinaryPath, still null.
  3. resolveHostBinary() therefore never takes its first branch,

{resolution: "local_override"}, and falls through to required_version.

  1. The app launches its pinned build, e.g. ~/.config/Claude/claude-code/2.1.219/claude.

Symbol counts across the whole bundle (grep -ao … | wc -l), which is what makes this
conclusive rather than suggestive:

| symbol | count | reading |
|---|---:|---|
| initLocalBinary | 1 | the definition alone — no call site exists |
| CLAUDE_CODE_LOCAL_BINARY | 1 | the discarded read |
| localBinaryInitPromise | 3 | one = null, two reads |
| localBinaryPath | 5 | one = null, one assignment inside the dead method, three reads |
| local_override | 4 | the unreachable resolution branch and its telemetry |

Reproduce

  1. Create an executable stand-in that logs and then execs the real binary:

``sh
#!/usr/bin/env bash
printf '%s ran\n' "$(date -Is)" >> "$HOME/stand-in-ran.log"
exec "$HOME/.config/Claude/claude-code/2.1.219/claude" "$@"
``

  1. chmod +x it and set CLAUDE_CODE_LOCAL_BINARY to its path.
  2. Launch Claude Desktop and start a Claude Code session.

Expected: stand-in-ran.log is created, and main.log carries
[CCD] LOCAL OVERRIDE: Using local binary at ….

Actual: the log file is never created. main.log shows
[CCD] Initialized with version 2.1.219 — the line immediately preceding the discarded
read, so the constructor certainly ran — and neither LOCAL OVERRIDE message, success
or failure. The session's own environment does contain CLAUDE_CODE_LOCAL_BINARY pointing
at the stand-in, while its argv[0] is the pinned build.

Four consecutive launches, all identical:

2026-08-05 17:28:00 [info] [CCD] Initialized with version 2.1.219
2026-08-05 18:18:13 [info] [CCD] Initialized with version 2.1.219
2026-08-05 19:00:43 [info] [CCD] Initialized with version 2.1.219
2026-08-05 19:40:17 [info] [CCD] Initialized with version 2.1.219

Suggested fix

Restore the assignment in the constructor, guarded on the variable being set, so
getLocalBinaryPath() has a promise to await:

const local = process.env.CLAUDE_CODE_LOCAL_BINARY;
if (local) this.localBinaryInitPromise = this.initLocalBinary(local);

initLocalBinary() itself needs no change — its access(path, X_OK) check and its two
log branches are correct as written.

Worth considering alongside it: the failure is silent because the only two log lines
that would name this path both live inside the method that never runs. One _.info
emitted when the variable is set but the override does not engage would have turned an
evening of investigation into a single grep. A dead feature that logs nothing is
indistinguishable, from outside, from a user error.

Why it cost real time

Every check that inspected the environment found the variable present and correct, and
concluded the mechanism was working — including checks inside the spawned session, because
the app faithfully propagates the variable it is ignoring. Only a check of the mechanism's
own footprint — did the stand-in leave a trace before exec replaced it — disagreed, and
that one was right.

View original on GitHub ↗