diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index 71471ec3bc..c9e68fd798 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,4 +1,23 @@ +## 07308543.1 (17-08-2026) +- Fix the "For your security, sign in again" prompt recurring on every restart again. The v1.37 + `safeStorage` patch (`86-claude_safestorage.sh` / `claude-safestorage-patch.js`) only knew how + to inject its opt-in after a leading `"use strict"` directive in the app's main bundle, and + refused to patch anything else. Confirmed live on the running add-on (Claude Desktop + 1.30096.1): the shipped main bundle (`.vite/build/index.pre.js`) no longer opens with a + `"use strict"` directive at all — it now opens directly with a bare IIFE — so the patcher has + been silently refusing to patch on every boot, `safeStorage.isEncryptionAvailable()` stayed + `false`, and the app's own log kept showing `Encryption not available, returning empty env + vars` exactly as before v1.37. `applyPatch()` now falls back to inserting the opt-in as the + bundle's first real statement when no directive is present, skipping past any leading BOM, + hashbang, or banner comment first so a directive hidden behind a comment is still found and + protected rather than pushed out of position zero. Verified by copying the live production + `app.asar` and running the patcher against it directly: the previously-refused bundle now + patches successfully, the marker lands correctly, a second run reports "Already patched", and + targeted unit tests cover the bare-IIFE, comment-hidden-directive, hashbang, and + unterminated-comment cases. +- One-time step after upgrading, same as v1.37: the previously-stored session is already stale, + so complete one sign-in from a computer; it then persists across restarts. ## 07308545 (2026-08-15) - Update to latest version from aaddrick/claude-desktop-debian (changelog : https://github.com/aaddrick/claude-desktop-debian/releases) - Upstream tag : v3.2.2+claude1.30096.1 diff --git a/claude_desktop/SIGN_IN.md b/claude_desktop/SIGN_IN.md index 3b5b0548a0..02c785becf 100644 --- a/claude_desktop/SIGN_IN.md +++ b/claude_desktop/SIGN_IN.md @@ -13,8 +13,10 @@ streamed desktop. offline until a fresh sign-in was done from a computer). v1.35 switched to `--password-store=basic` plus a cont-init script that re-syncs the persistent openbox `autostart` from the image on every boot — **but that flag alone does nothing**, and the bug - survived it untouched. Actually fixed in v1.37, which adds the application-side opt-in the - `basic` backend requires; see "Why v1.35 did not work" below. + survived it untouched. v1.37 added the application-side opt-in the `basic` backend requires; + see "Why v1.35 did not work" below. That patcher then regressed silently when upstream's + bundle output changed shape — fixed again in 07308543.1; see "Why v1.37 stopped working" + below. - **Planned only:** Problem A (in-desktop browser for OAuth) is intentionally not implemented. The image ships no browser; complete the login with the user-side workaround below. @@ -170,10 +172,40 @@ The third row is the one that matters: it is the restart survival this add-on ne reaches upgrades, not just fresh installs. 4. `gnome-keyring` stays out of the Dockerfile. +### Why v1.37 stopped working + +`claude-safestorage-patch.js` only knew how to inject its opt-in *after* a leading +`"use strict"` directive in the app's main bundle, and refused to patch (leaving the app +unpatched and the session un-persisted) if that directive wasn't there. Confirmed live on the +running add-on: Claude Desktop 1.30096.1's main bundle (`.vite/build/index.pre.js`) no longer +opens with a `"use strict"` directive — it now opens directly with a bare IIFE +(`(function(){try{var e=typeof window...`). Upstream's build output changed shape at some point +after v1.37 shipped, the patcher's one injection point stopped existing, and it had been +silently refusing to patch on every boot since — the app's `main.log` kept showing exactly the +same `Encryption not available, returning empty env vars` warning documented above, and the +session went back to not surviving restarts. + +`applyPatch()` now falls back to inserting the opt-in as the bundle's first real statement when +no `"use strict"` directive is found, rather than refusing outright. It skips past any leading +BOM, hashbang, or banner comment first (`skipPrologue()`), so a directive hidden behind a +comment is still found and protected instead of being pushed out of the first-statement +position by a naive prepend — Vite/esbuild banners commonly put a license comment ahead of the +directive. A bundle with no directive at all has nothing to protect, so prepending the opt-in +there is unconditionally safe: the injected code is a complete `try{}catch(e){}` statement, and +a statement can never merge with what follows it via ASI the way a bare expression could. + +Verified by copying the live production `app.asar` and running the patcher against it directly +(outside the container's boot sequence): the previously-refused bundle now patches +successfully, the marker lands at the front of the main entry, a second run correctly reports +"Already patched" (idempotent), and unit tests cover the bare-IIFE, comment-hidden-directive, +hashbang, and unterminated-comment cases. + ### One-time step after upgrading The previously-stored session is already stale. Complete **one** sign-in from a computer (mobile still can't finish the OAuth flow itself, per Problem A) — the session then persists -normally and dispatch stays online regardless of which device connects first afterward. +normally and dispatch stays online regardless of which device connects first afterward. This +applies again after the 07308543.1 fix above, since the affected sessions were never persisted +in the first place. --- @@ -185,7 +217,10 @@ normally and dispatch stays online regardless of which device connects first aft - `claude_desktop/rootfs/etc/cont-init.d/86-claude_safestorage.sh` and `claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js` — new in v1.37; the app-side `safeStorage` opt-in that makes `--password-store=basic` actually take effect. + `claude-safestorage-patch.js` updated again in 07308543.1 to also patch bundles with no + leading `"use strict"` directive, and to look past leading comments/hashbang when deciding + whether one is present. - `claude_desktop/Dockerfile` — corrected stale comment (gnome-keyring is not installed). -- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35, then v1.37. +- `claude_desktop/CHANGELOG.md` / `config.yaml` — v1.35, then v1.37, then 07308543.1. Problem A (in-desktop browser for OAuth) remains planned-only; not touched by this change. diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index a67e6663b8..2814570839 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -136,5 +136,5 @@ schema: slug: claude_desktop udev: true url: https://github.com/alexbelgium/hassio-addons -version: "07308545" +version: "07308545.1" video: true diff --git a/claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js b/claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js index b6023b78c3..2b19f60299 100644 --- a/claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js +++ b/claude_desktop/rootfs/usr/local/bin/claude-safestorage-patch.js @@ -128,24 +128,98 @@ function integrityOf(buf, blockSize) { return { algorithm: 'SHA256', hash: sha256(buf), blockSize, blocks }; } -/* Insert the opt-in after the bundle's leading "use strict" directive. It must go *after* it: a - * directive prologue only takes effect as the very first statement, so prepending would silently - * drop the whole main process out of strict mode. +// Any of the four ECMAScript LineTerminator code points — not just "\n". A //-comment or an ASI +// boundary ends at the first of these, and using a bare "\n" search for that would let a CR- or +// U+2028/U+2029-terminated line swallow real code as "still the comment/still on this line" and +// misplace the insertion point deep inside the bundle instead of before it. +const LINE_TERMINATOR = /[\n\r\u2028\u2029]/; + +/* Skip a leading BOM and hashbang line. Only meaningful at byte 0 — called once, before any + * directive scanning. */ +function skipBomAndHashbang(source) { + let i = source.charCodeAt(0) === 0xfeff ? 1 : 0; // BOM + if (source.startsWith('#!', i)) { + const m = LINE_TERMINATOR.exec(source.slice(i)); + i += m ? m.index + 1 : source.length - i; + } + return i; +} + +/* Skip whitespace and comments starting at i. Returns the next index, or -1 for an unterminated + * block comment (caller refuses rather than guesses). */ +function skipWhitespaceAndComments(source, i) { + for (;;) { + const rest = source.slice(i); + const ws = /^\s+/.exec(rest); + if (ws) { + i += ws[0].length; + continue; + } + if (rest.startsWith('//')) { + const m = LINE_TERMINATOR.exec(rest); + i += m ? m.index + 1 : rest.length; + continue; + } + if (rest.startsWith('/*')) { + const end = rest.indexOf('*/'); + if (end === -1) return -1; + i += end + 2; + continue; + } + return i; + } +} + +// A single-line string literal: no raw line terminator in its content (a real one would need an +// escaped line continuation, which this deliberately doesn't special-case — failing to match +// just means the prologue scan below stops there, which is always safe, see applyPatch). +const STRING_LITERAL = /^(['"])(?:\\.|(?!\1)[^\\\n\r\u2028\u2029])*\1/; + +/* Scan the bundle's full leading directive prologue: every consecutive ExpressionStatement made + * of nothing but a string literal, per how ECMAScript directives actually work. A directive + * prologue can hold more than one entry, and "use strict" only has to appear *somewhere* in it, + * not first — so this treats every leading directive as needing protection, not just one + * specifically named "use strict". Returns the index right after the full prologue (which is + * also correct as "no prologue, insert here" when there wasn't one), or -1 when a leading string + * literal isn't cleanly terminated as its own statement — ambiguous whether it's a directive at + * all, refused rather than guessed at. */ +function scanDirectivePrologue(source, start) { + let i = start; + for (;;) { + const next = skipWhitespaceAndComments(source, i); + if (next === -1) return -1; + const rest = source.slice(next); + const m = STRING_LITERAL.exec(rest); + if (!m) return next; // not a directive; prologue (possibly empty) ends here + const after = rest.slice(m[0].length); + if (after[0] === ';') { + i = next + m[0].length + 1; + } else if (after === '' || LINE_TERMINATOR.test(after[0])) { + i = next + m[0].length; + } else { + return -1; // "use strict" + x and friends: not unambiguously a directive + } + } +} + +/* Insert the opt-in right after the bundle's full leading directive prologue (BOM/hashbang, then + * any run of string-literal-only statements — "use strict" among them if present). It must go + * *after* the whole prologue, not just after the first entry: a directive prologue only takes + * effect when its members are the very first statements, so inserting between two of them, or + * ahead of all of them, would silently drop the file out of strict mode just as surely as + * inserting ahead of a lone "use strict" would. * - * Returns null — meaning "refuse to patch" — for anything that is not unambiguously a directive. - * `"use strict" + x` is an expression, not a directive, and injecting into it would produce a - * syntax error, so the directive is only accepted when it is terminated by its own semicolon, a - * line break, or end of input. */ + * When there is no prologue at all (observed from Claude Desktop 1.30096.1 onward, whose main + * entry opens with a bare IIFE instead), there is nothing to preserve: PATCH lands at the same + * position anyway, as the file's first real statement. A `try{}catch(e){}` statement can never + * merge with whatever follows via ASI — unlike a bare expression, a statement is not a valid + * left-hand side for anything a following token could continue — so this is unconditionally + * safe once placed after any banner comment / hashbang / directive prologue. */ function applyPatch(source) { - const m = /^\s*(['"])use strict\1(;?)/.exec(source); - if (!m) return null; - const rest = source.slice(m[0].length); - const terminated = m[2] === ';' || rest === '' || /^[\r\n]/.test(rest); - if (!terminated) return null; - // Supply the terminator when the directive relied on ASI; without it the injected code would - // continue the string-literal expression instead of following it. - const sep = m[2] === ';' ? '' : ';'; - return source.slice(0, m[0].length) + sep + PATCH + rest; + const start = skipBomAndHashbang(source); + const end = scanDirectivePrologue(source, start); + if (end === -1) return null; + return source.slice(0, end) + PATCH + source.slice(end); } function writeAll(fd, buf) { @@ -203,7 +277,7 @@ function main() { const patchedSource = applyPatch(original); if (patchedSource === null) { - fail(`${mainRel} does not begin with a recognized "use strict" directive; refusing to patch`); + fail(`${mainRel} opens with an ambiguous "use strict"-like string literal; refusing to patch`); } const patched = Buffer.from(patchedSource, 'utf8');