Concrete clode-under-Node divergences from native Claude Code, to triage and fix.
(Strategic feasibility risks live in LONG-TERM.md; in-flight designs in
docs/superpowers/.)
NEXT UP — Phase 3: TUI paints (M1) + human-verified turn (M2) + AGENTIC TOOL USE all work under tjs; M3 (render parity) next
Landed today (15 commits, e3e5a15..2d1f249; tree clean): UAF/SIGSEGV fixed
(ASAN); 5 interactive-path gaps (Intl polyfill, Intl-new-optional, setEncoding,
child.stdin Writable, +the persistent-shell stdin hang); agentic tool use (Bash
tool: memoryUsage/cpuUsage, fs.promises.open+O_*, tjs.spawn numeric-fd inherit). So
under tjs now: M1 paint + M2 human turn + slash commands + Bash tool all work. v0
API-surface gate shipped (scripts/apicheck.mjs). Full suite (with TUI-live-render):
514/474 pass/2 fail/1 todo — the 2 fails are STALE ws tests (see below), not tjs.
Endgame design (untracked): docs/superpowers/specs/2026-07-08-api-surface-gate-design.md.
Recommended first move — the ws / bundled-deps decision (brick #1 of self-attesting binaries):
- Settle the shipped-binary deps contract: SEA
deps.tarbundling is the only model for shipped binaries; a missing REQUIRED dep = broken build → fail loud (user's rule). - Revisit
8dc4947(non-fatalrequire('ws')was a bring-up concession) — restore fail-loud-at-require for required deps, or scope the lazy stub behind a dev flag. - The 2 stale tests (
websocket.test.cjs,e2e-tui.test.cjs) resolve as a CONSEQUENCE of that decision — do NOT touch them in isolation.
Then / in parallel (ranked):
- apicheck v1 = the embeddable oracle: recording-Proxy per-module coverage %,
checked-in golden baselines + baseline manifest, and broaden the corpus to include
tool-use turns (today's OVr bug was invisible to headless
-p— the corpus MUST exercise tools). This artifact IS the futureclode selftest. clode selftest(self-attesting binary): wire the corpus+baselines into a shippedclode selftest [--json]— a per-capability confidence map users run on their own box (offline, node-free, token-free). Delivery vehicle for the oracle principle; design doc.- M3 (render parity): tjs interactive render ~1.2MB vs node ~8KB/turn (redundant full redraws). Last phase-3 milestone; non-fatal.
- Upstream-txiki batch (awaiting go-ahead):
txiki-spawn-fail-uaf,txiki-stream-write-sync-number,txiki-spawn-inherit-fd, the quickjs-ng v-flag\p{}regexp bug, + the phase-2 batch (sync-fs, sync-spawn CLOEXEC, no-origin, netbsd, default-stack-size). - SPARC verify-next: does the
-ppath instantiate any wasm (tokenizer), or is all wasm behind the TUI (Yoga)? If-pis wasm-free, headless-SPARC decouples from wamr-big-endian. - Minor:
process.resourceUsagestill undefined (unneeded so far; add if a path hits it).
AGENTIC TOOL USE WORKS (d4e197d, independently verified): a real Bash-tool turn
runs a shell command and returns output under CLODE_ENGINE=tjs. Root cause was a
cluster of tool-path shim gaps (NOT the async-gen codegen bug): process.memoryUsage/
cpuUsage (broke ALL tools — the runner records an rss baseline before every call);
plus the Bash tool's fd-redirection (fs.promises.open+O_* constants, and REAL
numeric-fd inheritance in tjs.spawn via txiki-spawn-inherit-fd.patch → UV_INHERIT_FD,
rebuilt+re-signed; child_process fd passthrough). Suite 128/124 pass/0 fail/4 skip. Found
by subagent + watchdog verification (which caught an incomplete first fix and a
confabulated co-diagnosis — dogfood of the gate's verify-don't-relay principle).
The interactive Claude Code TUI now RENDERS under CLODE_ENGINE=tjs. The
tui-diff oracle went 13 → 1603 bytes (host node 2062); the screen shows the
"Claude Code v2.1.202 / Opus" box, the ❯ prompt, "? for shortcuts", and
"Not logged in · Run /login". Full trail: spike/quickjs/results/phase3-m1-tui-boot.md.
How we got there (parallel subagent workstreams, 2026-07-08):
- Shim TTY layer (tasks 1–3, reviewed): real
tty.ReadStream/WriteStreamovercore.TTY(isTTY, columns/rows, resize/SIGWINCH,setRawMode, async keystroke pump + paused-modereadable/read(), side-effect-freeisatty) —94905f5..6521c1f,e00506f. - Boot walls fixed via the node-vs-tjs render-byte differential (
efaf6d7,e00506f): legacyconstantsmodule,fs.utimes/lutimes,fs.StatsDate accessors,tty.WriteStreamcursor/erase methods, dynamicimport()(→require), paused-mode stdin. - The paint blocker (root cause + fix,
bcf53eb): a quickjs-ng libregexp bug — Unicode property escapes\p{…}/\P{…}under thev(unicodeSets) flag match non-members / miss members (correct underu). Cascade:string-width@≥7baseVisible("t")→""→codePointAt undefined→get-east-asian-widththrows during the REPL module's top-level init insidelaunchRepl; swallowed upstream soink.render()is never called. Fix: the loader downgradesv→uon module-source regex literals that use\p{}but none ofv's exclusive features (semantic no-op on a correct engine). Char testtest/node-shim-vflag-regex.test.cjs. Proper long-term fix belongs in quickjs-ng libregexp — upstream candidate. - The API method-level coverage inventory (
spike/quickjs/results/phase3-api-coverage.md)- 3 batches of proactive gap-fills (
17fe218,0b0723a,4f7add3: assert, querystring, string_decoder, fs/util/url/crypto/zlib/stream.finished). Key result: the-p PONGand TUI paths fire 0 missing-method walls — the real blockers were engine/behavioral bugs, not missing APIs (validates the hybrid strategy).
- 3 batches of proactive gap-fills (
- The "TUI fetch hangs" theory was a measurement artifact (
7f0fa3a): Ink patchesconsole.errorin its ctor, swallowing the trace logs; the fetch actually resolves 3/3 (proven via raw fd-2 writes). No fd-race/timer/TLS bug on the fetch path. One genuine hardening fix did land:__tjs_spawn_syncwas leaking live fds into sync children — CLOEXEC fix7b36cf5.
- Shim suite now fully GREEN: 118 tests / 114 pass / 0 fail / 4 skip;
-p PONG- TUI render (1603 bytes) verified. The previously-red
child_processENOENT test was NOT "codegen fragility" — ASAN pinpointed a concrete heap-use-after-free:tjs_spawn's spawn-launch-failure path freed theuv_process_thandle synchronously while libuv still owned it (a lateruv__run_closing_handleswrote into freed memory). FIXED at the source:txiki-spawn-fail-uaf.patch(commite3e5a15, upstream candidate) releases the handle via the asyncuv_closepath instead of the direct free. So the "layout-sensitive SIGSEGV lottery" is closed.
- TUI render (1603 bytes) verified. The previously-red
- M2 ACHIEVED (2026-07-08): human-verified interactive turn on a real console under
CLODE_ENGINE=tjs— "what is 6 + 7" → correct answer, TUI stayed coherent. Four tjs-only interactive gaps fixed en route (each traced via a bundleae.stackprobe in the extractedcli.cjs, fixed as a shim gap, locked by a node-parity test):f2afbe8Intl polyfill (modules/intl.cjs— quickjs-ng has no Intl; the loader only shimmed Segmenter, sonew Intl.NumberFormatwasnew undefined());d39fd4dReadable.setEncoding (hook/subprocess reader; now StringDecoder-backed);4bed83achild.stdin as a real Node Writable (hook runner writes stdin; fire-and-forget over getWriter since tjs child-stdin writes don't resolve);f7da37eIntl legacy constructors new-optional — the TUI callsIntl.DateTimeFormat(...)WITHOUTnew(legal ECMA-402), ES6classthrew; this one appeared ONLY on a real console turn (headless-pnever renders dates → live proof corpus coverage is the lever). Suite 124/120 pass/0 fail/4 skip. Auth "Not logged in" was NOT a tjs bug — earlier testing was over SSH (login Keychain locked →securitycan't read the subscription credential); on a real console (unlocked) auth + the turn work. M3 (render parity) is the remaining phase-3 milestone; the tjs interactive render is byte-heavy (~1.2MB vs node ~8KB/turn — redundant full redraws, non-fatal), an M3/efficiency item. - Agentic tool use (Bash tool) fixed (
1222660, tjs C, upstream candidate): the Bash tool feeds short commands to a PERSISTENT shell via stdin and every write hung —mod_streams.creturnedJS_TRUEon theuv_try_writesync-complete path where the JS sinks (process.js/udp.js) expect a byte-count NUMBER, so they awaited an onwrite never scheduled. Fix: return the count. Data was delivered; only the write-ack was missing (also blocked close/EOF). This is the substrate for a coding agent running commands under tjs.txiki-stream-write-sync-number.patch. - Build-environment follow-ups (bit the UAF rebuild — recorded in PINS.md): the
~42k AppleDouble
._*sidecars must be purged before building;build-tjs.mjs's strictgit applycan't re-sequence the overlapping sync-fs/sync-spawn patches on an already-patched tree (GNUpatch --forwardworks); and the binary needs acodesign -s - --forcere-sign after copying off the build dir (else exec dies "code signing error"/SIGKILL). Worth hardeningbuild-tjs.mjsfor these. - Upstream candidates grew: quickjs-ng
v-flag\p{}regexp bug; thetxiki-spawn-fail-uafUAF; plus the phase-2 batch andtxiki-sync-spawnCLOEXEC. Plan:docs/superpowers/plans/2026-07-08-phase3-tui-under-tjs.md; design:docs/superpowers/specs/2026-07-08-universal-binaries-phase3-tui-design.md.
Goal: given a new upstream binary → extracted cli.cjs, tell us BEFORE shipping
what the bundle needs and where tjs diverges from node — so agents polyfill from a
work-list instead of the user hand-reproducing breakage. Full design (untracked, on
disk): docs/superpowers/specs/2026-07-08-api-surface-gate-design.md.
Two axes (do not conflate): presence (is the API defined — enumerable via the
wallProxy [wall] misses) and correctness (defined but wrong — the v-flag
regexp / UAF / setEncoding class — only a node-vs-tjs behavior diff sees these).
Conclusive static enumeration is undecidable (computed dispatch); the reachable
endgame is empirical + regression-gated (how Bun/Deno do Node-on-non-Node), with the
corpus as the only real lever and every field miss becoming a permanent case.
- v0 SHIPPED:
scripts/apicheck.mjs— seed corpus ofclodeinvocations run under node AND tjs; reports the[wall]miss union (Axis 1), node-vs-tjs exit/ deterministic-stdout divergences (Axis 2), and the cross-version require-target set-diff; exits non-zero as a CI gate. First run (2.1.204): PASS — 0 walls, 0 divergences, 2.1.198→2.1.204 require-set unchanged. - v1 (next): recording-Proxy hit-ledger for true per-module coverage %; checked-in baseline manifest + known-good outputs; broaden corpus (tool uses, slash commands, flags, hooks, MCP). v2: import deps'/Node's test suites as differential corpora; frame-level TUI render parity (folds into M3).
- Oracle principle (first-class): where the reference oracle doesn't exist, the
gate's golden-output baseline becomes the oracle.
node-differential only works on platforms that don't need tjs; on NetBSD/SPARC there's nonodeto diff against, so capture canonical/deterministic/normalized outputs (exit codes, crypto digests of fixed inputs, fixed-content frames) on a reference platform, check them in, and diff the exotic platform against the record.nodemoves UPSTREAM (re-mint the baseline per upstream bump), doesn't vanish. This makes v1's baseline manifest load-bearing, and it's exactly what catches big-endian divergences. Bonus: golden slice runs offline/token-free. - Platform reach (grounded): tjs (portable C) reaches where Bun can't.
NetBSD/aarch64 headless PROVEN (M4, same-endian OS port). SPARC = the endianness +
strict-alignment phase change. The bundle uses wasm, but dominantly Yoga (layout,
TUI-only) — so headless
-pis likely wasm-free and SPARC-headless may be reachable before solving wamr-big-endian (the TUI needs it; wamr already works on aarch64, so the open Q is narrowly big-endian). Byte-match surface (createHash×90, DataView×73, Buffer.from×331) is what the oracle principle guards. VERIFY-NEXT: does-pinstantiate any wasm (tokenizer)? Full notes in the design doc. - Activation policy (decided): none of this touches the user hot path.
extract/launch stay untouched (no marker, no log — CI notices new versions by diffing itself); users opt in only via explicit env flags (CLODE_SHIM_TRACE, futureCLODE_API_LEDGER), off by default (residue when off = a load-time check, never per-call); dev/CI lean hard on the full gate out-of-band. Recording proxy is constructed only when its flag is set, never in correctness runs (it perturbs). - Caution baked into the harness: THIS Claude Code session's env carries
CLAUDE_CODE_BRIDGE_SESSION_ID(child bundle auths via the parent bridge) — fine for coverage/parity, but strip it to test real subscription auth.
Phase 2 is functionally complete (2026-07-07). Both round-trip milestones PASS with live subscription auth (no API key):
- M3b (darwin/arm64): 22/22
PONGincl. under CPU load —spike/quickjs/results/phase2-m3b-subscription-auth.md(d7b2166). The feared native fd-race is non-reproducible on clean main (trigger gone since7ce7a89); latent-risk oracle + instrumentation pointer recorded in the doc. - M4 (NetBSD 10.1/aarch64 guest): 3/3
PONG, patched tjs built in-guest with pkgsrc gcc12 in a ≈21-min fully-scripted run —spike/quickjs/results/phase2-m4-netbsd-aarch64.md. Five build walls closed (gcc12-for-ada, mimalloc-3.2.7-broken-on-NetBSD upstream regression, clang-only#pragma regionunder -Werror, + 3 real portability bugs now in committedpatches/txiki-netbsd-portability.patch).
Remaining per the design:
- Take stock (the M4 milestone's second half): short report — actual shim effort vs gate-4 predictions, remaining walls, phase-3 (TUI) + platform-sweep outlook.
- Upstream submissions (prepared, awaiting user go-ahead per PINS.md): js_exepath
- repl-eof-spin (quickjs-ng); sync-fs, sync-spawn, no-origin-header (txiki); NEW
from M4: txiki-netbsd-portability (txiki PR), mimalloc NetBSD compile regression
(report),
#pragma region-vs-gcc-Werror (txiki report).
- repl-eof-spin (quickjs-ng); sync-fs, sync-spawn, no-origin-header (txiki); NEW
from M4: txiki-netbsd-portability (txiki PR), mimalloc NetBSD compile regression
(report),
Spec 1 (infra) SHIPPED 2026-07-03: libexec/clode-paths.cjs choke point (one
CLODE_STATE_ROOT knob seals npm + SEA), CLODE_NODE canonicalized in run-all
(shim footgun gone), per-file sandbox in test_helper.bash, preflight+postflight
guard (test/hermetic-guard.cjs), CLODE_NO_WATCH=1 in the sandbox (no detached-
watcher teardown races). Design: docs/superpowers/specs/2026-07-02-hermetic-testing-design.md.
Spec 2a (bats→node foundation + bulk) SHIPPED 2026-07-03: test/e2e.cjs is a
constructed-clean node harness (sandbox/runClode/mkProvider/fakeNpm; every
spawn's env built from scratch — nothing from process.env — with CLODE_DEPS
pointed at a seeded no-.deps-sig store so ensureDeps takes its npm-free
user-managed opt-out and the bundle boots offline WITHOUT npm; the bash harness had
instead leaned on npm-on-PATH + an npm cache). The 16 plain-subprocess .bats files
were converted 1:1 to node:test (one node test per @test, positive AND negative
assertions preserved, matched to real launcher output) and deleted in-commit, and
test_harness_isolation.bats was dropped (invariant now structural). ls test/*.bats
is down to the 4 PTY/TUI files. Plan: docs/superpowers/plans/2026-07-03-bats-to-node-2a.md.
Note found en route: the plan's two selfupdate labels were swapped — test_selfupdate
(no underscore) is the extracted-bundle CACHE lifecycle; test_self_update (underscore)
is clode update via a file:// releases fixture + signals digest.
Spec 2b (PTY/TUI tail) SHIPPED 2026-07-03 (plan
docs/superpowers/plans/2026-07-03-bats-to-node-2b.md): the last 4 .bats are gone,
test_helper.bash is deleted, and run-all.sh runs node tests only — the suite is now
pure node:test (Windows-portable, no bash framework). What landed:
test/e2e-pty.cjs— node:test PTY capture harness wrappingtui-screen.cjs(makeWsWorlds/seedClaudeProfile/capture);seedClaudeProfileis the minimal cwd-keyed~/.claude.json(onboarding +projects[cwd]trust) that gets a no-keystroke capture past onboarding + the trust prompt.test_tui→e2e-tui.test.cjs(all 4, incl. the once-quarantined #60) andtest_doctor_parity→e2e-doctor-parity.test.cjs+refresh-doctor-golden.sh.test_update+test_launcherverify-and-deleted (coverage already ine2e-keying/e2e-selfupdate/e2e-resolve).
Key discovery: the TUI/doctor render tests are NOT hermetic — they spawn the REAL
Claude Code bundle (the fake fixture can't render the Ink welcome box; #60's ws fatal
only fires on real WebSocket construction). Worse, the real bundle probes the macOS
login Keychain and pops system dialogs. So both live-render files are gated behind
CLODE_LIVE_RENDER=1 (opt-in) and SKIP in the default offline npm test — no
bundle spawn, no Keychain, no network. clode's ws-fail-loud contract stays covered
hermetically by test/websocket.test.cjs; the comparator by doctor-parity.test.cjs.
Residual (small):
- a Windows-portable (node-based)
fakeNpmto replace the Spec 2a.shshim (test/e2e.cjs); - fold the
clode-runupdate-guardCLODE_CACHEsemantics decision if it recurs.
Curate the live /doctor parity allowlist (e2e-doctor-parity test 2, currently
skipped). The opt-in CLODE_LIVE_RENDER=1 run (2026-07-03) showed the strict
native-vs-clode comparison reds on pure environment noise, not clode bugs. Want (user
decision): keep a STRICT comparison with a curated allowlist of areas we deem
unimportant, failing on any other diff — that's the upstream-/doctor-format drift
signal. To get there:
- Kill wrapping noise: give clode's capture the REAL render deps (string-width/
strip-ansi/wrap-ansi), not the
seedRenderDepsfakes, so line-wrapping matches native (amakeWsWorldsvariant that copies/symlinks the real~/.local/share/clode/node_modulesrender deps + fakews). The original bats used the real store for exactly this reason. - Normalize section-title status glyphs (
⚠/✔/◯/✗) indoctor-parity.cjsparseScreen, so a status flip (e.g. Diagnostics✔→⚠from a Keychain-cancel) doesn't cascade into mis-grouped "ADDED/DROPPED block" noise. - Extend the allowlist to the volatile sections:
Updates(version fetch / "Failed to fetch versions"),Remote Control(auth/session lines), and the macOS Keychain-writability warning inInstallation warnings/Diagnostics. - Then un-skip test 2. Validation needs live captures (Keychain dialogs each cycle), so
batch it. Comparator logic itself stays covered by
doctor-parity.test.cjs.
The suite is not hermetic: it leaks the real machine's state in and its own fixtures out. Three symptoms observed in a single session on 2026-07-02:
- Fixtures leak OUT into the real store: four
0.0.0-clode-teststub deps (semver, string-width, strip-ansi, wrap-ansi) were seeded into~/.local/share/clode/node_modulesby a pre-4a99d2btest run, breakingclode-watch's real-semver test and degrading real clode runs (see the dedicated item below). - Real environment leaks IN — provider on PATH:
test_resolve.bats#5 ("no provider yields exit 1") depends on the ambient PATH/HOME; a real~/.local/bin/claudeand/or the caller's node choice can defeat its isolation. - Real environment leaks IN — node identity: the same test passes only when
CLODE_NODE=/opt/pkg/bin/node(a real binary) and fails under the defaultcommand -v nodeasdf shim, because the shim re-resolves under the test's minimal PATH. Correct execution silently depends on an env var the runner defaults wrong on this box.
Want: a deliberate design (own superpowers spec → plan) for test hermeticity that:
- seals both directions — no test can read or write the real
~/.local/share/clode,~/.cache/clode,~/.local/bin, or ambient PATH provider; every test runs against a private, disposable HOME/store/cache/PATH. - works identically for npm-layout AND SEA execution (the two runtime shapes clode ships), so a test passing under one can't rot under the other.
- is structurally hard to violate as tests are added — e.g. a single enforced
sandbox fixture/harness all tests inherit, plus a preflight/CI guard that fails
if the real store shows
*-clode-testdeps or a test process escapes the sandbox — rather than per-test discipline that erodes. - pins/handles the node identity explicitly (which node runs the suite) so correctness doesn't hinge on an unset env var.
Symptoms 1-3 are the concrete acceptance cases the design must close.
Coverage temporarily quarantined (restore in the bats→node conversion): hermeticity
work (2026-07-02) surfaced that test_tui.bats #58/#60 silently rode the dev's real
~/.claude.json (onboarding + per-project trust). #58 fixed by seeding onboarding in
the sandbox; #60 ("TUI fails LOUD when the ws ext-dep is missing") is skipped — its
repro needs a hermetic per-project trust-state fixture (Claude Code's real
~/.claude.json is ~76KB, keyed by projects["<path>"] trust flags). The conversion
must rebuild that as a golden fixture and un-skip #60. Also quarantined: the anti-hermetic
cases in test_update/test_launcher/test_watch/test_doctor_parity.
On 2026-07-02, four 0.0.0-clode-test stub packages (semver, string-width,
strip-ansi, wrap-ansi) were found sitting in the user's real dep store at
~/.local/share/clode/node_modules, seeded by a test run before commit
4a99d2b "fix(test): never seed fake deps into an inherited CLODE_DEPS store".
They made clode-watch's versionGt uses real semver test fail (the fake
compare() can't order prereleases) and would silently degrade real clode runs
(e.g. a fake string-width returns wrong widths). Removed the four stubs by hand;
they self-heal on the next run.
- Investigate: confirm
4a99d2bfully closes the seeding path — was that the only vector, or can any other test still write into an inherited/realCLODE_DEPS/~/.local/share/clode? Reproduce the pre-fix leak to be sure the fix covers it. - Prevent: add a guard so a test can never mutate the real store — e.g. tests
that need a fake-dep store must point
CLODE_DEPSat a tmp dir (see the isolate-clode-runs guidance), and consider a CI/run-allpreflight that fails if~/.local/share/clode/node_modules/*/package.jsonreports a*-clode-testversion.
Open question, observed live: the bottom status line doesn't surface the expected
gh auth login hint. Likely the same class as the auto-update nudge — status-line
content the bundle computes that clode's environment (or a shim gap) changes.
- Investigate: reproduce, compare against native, and locate where the bundle
builds that status-line item — is the hint suppressed, mis-evaluated (e.g. a
gh/auth probe behaving differently under the Node host), or rendered and just not where expected?
Upstream keeps versioned binaries at ~/.local/share/claude/versions/<ver> with a
~/.local/bin/claude symlink to the active one. clode's cache (~/.cache/clode/<KEY>)
keys on the provider binary — <ver> when the path encodes one, else
<basename>-<size+mtime> — and now also stores an .extractor-sig so a changed
extractor re-extracts. The two schemes aren't aligned: our <KEY> dirs don't map
cleanly onto upstream's versions/<ver> layout, and the basename-sig fallback is
opaque.
- Want: make clode's per-version storage mirror upstream's more directly (e.g.
a
versions/<ver>layout with an "active" pointer), so it's obvious which extracted bundle corresponds to which upstream version, easy to GC stale ones, and natural to diff/inspect side by side. - Relates to: the extractor-fingerprint re-extract (cache validity now depends
on
(binary, extractor)), and the cache-key logic inbin/clode(cache_key).
Idea, deferred. At launch, have bin/clode pass a short --append-system-prompt
line telling the model it's running under clode and to update via "$CLODE_SELF" update (clode is not necessarily on PATH; CLODE_SELF is already exported), never
claude update. The flag feeds the bundle's shared system-prompt builder (Kn1),
so it reaches interactive sessions.
- Why maybe-later: the
PreToolUse(Bash)update hook (in the complete-update-interception design) already denies model-issuedclaude update/upgradewith a just-in-time message pointing at the right command, which covers the common case. A proactive nudge would add value mainly by (a) reducing attempts up front and (b) catching non-claude-named update attempts the hook's regex won't match (e.g.npm i -g @anthropic-ai/claude-code). - Revisit if: the model keeps reaching for upstream update paths despite the
hook, or non-
claudeupdate attempts show up in practice. - Tradeoff:
--append-system-promptis single-value/last-wins, so a user's own value would override clode's. If stacking is needed, the bundle also concatenates anappendSystemPromptfrom settings insideKn1.
Port libexec/extract-claude-js (and, in time, libexec/inspect-claude-bundle)
from Python to JavaScript/Node. Worth doing on its own merits, and it removes a
blocker to single-binary deployment.
- Why it's sensible independent of SEA:
- Same runtime as the thing it processes. The artifact is a JS bundle that clode already runs under host Node; carving/transforming it in that same Node avoids a language boundary in the middle of the pipeline.
- One fewer host dependency. clode targets machines where toolchains are
scarce (NetBSD/pkgsrc, old macOS, non-x86); requiring a new-enough
python3cuts against that. Node is already required to run anything, so the extractor can lean on it instead. (Aligns with the portability mission inLONG-TERM.md.)
- Why it also matters for SEA: Node SEA packs JS + Node only, so a JS extractor is a hard prerequisite for the single-binary story below.
- Port faithfully — same loud-failure contract. Preserve, behavior-for-behavior:
the text-marker carve (
carve_blocks), the prelude, and every splice (patch_autoupdater,patch_native_autoupdater, the/doctoranchors) with their exactly-one-match-or-fail-loud discipline, plusverify(residual-NUL / import-meta checks). Theinspect-claude-bundle --strictgate and its anchor set should move too (or stay the source of truth until ported). Keepextract_if_needed's extractor-signature re-extract working across the language switch (the sig is over the extractor file — a rewrite naturally busts caches once). - Open question — translate the tests too? The Python tests (
test/test_extract.py,test/test_inspect.py) would naturally become Node tests alongside their subjects. Less clear for the shell/bats suites: they exercise the POSIX-sh launcher (bin/clode) directly, so untilbin/clodeis itself JS (the SEA work below), driving shell behavior from Node tests is awkward. Likely split: extractor/inspector tests → Node; launcher tests stay bats until the launcher moves. Revisit per-component as each is ported. Net goal where it lands: fewer test-host deps (no pytest; eventually no bats), one toolchain. - Relates to: the "Port the toolchain to JS" prerequisite of the Node SEA entry below (this is the extractor half of it).
DONE (2026-07-01): Isolate the fail-loud ext-dep tests so the repo can have a populated root node_modules
Implemented. The fail-loud children now load a temp copy of the (self-contained)
bun-shim.cjs from OUTSIDE the repo tree, with cwd in that temp dir, via the shared
helper test/isolated-shim.cjs (runShimChild). require("ws") (in the shim AND in
the -e body) walks a clean chain that can never reach <repo>/node_modules, so a
stray root npm install can no longer make the deps resolvable and defeat the
"dep ABSENT" assertions. NODE_PATH is still honored (appended after the clean walk)
for the fake-dep cases. Updated: test/{semver,websocket,yaml,text-utils}.test.cjs
(snapshot-rewrite.test.cjs needed no change — it uses the shim in-process and
doesn't assert dep-absence). Proven green WITH a fake root node_modules present.
Original note below for context.
Discovered during the J5 test-harness port (2026-06-30): a plain npm install at
the repo root breaks the test suite. The "fail-loud" ext-dep tests
(test/semver.test.cjs, text-utils, websocket, yaml, snapshot-rewrite,
and a bats case) assert that clode dies loudly when its runtime deps
(semver/ws/yaml/string-width/strip-ansi/wrap-ansi) are absent — and
they detect absence by the bun-shim resolving those names via an upward walk that
reaches the repo-root node_modules. So any npm install that populates root
node_modules with the dependencies block makes clode find those deps and the
fail-loud assertions stop firing → red suite. (Reproduced at a clean commit in
both worktrees.)
- Current workaround (in place): J5's PTY harness deps live in a separate
test/package.json→test/node_modules(gitignored, not shipped), keeping rootnode_modulesempty. Works, but it means the repo can never have a normal populated rootnode_modules(for devtools, type stubs, etc.) without breaking tests — a latent footgun for any future contributor who runsnpm install. - Proper fix: make each fail-loud test run its child
nodein an isolated module-resolution context that GUARANTEES the dep is unresolvable regardless of rootnode_modules— e.g. spawn the child withNODE_PATH=''AND a cwd/HOMEwhose upward walk can't reach the reponode_modules(a temp dir), or an explicit empty--require-shimmed resolver. Then the assertion tests "dep truly absent" independent of the repo's install state, and the root can be populated normally. - Payoff: removes the "npm install breaks the suite" landmine; lets the J5
harness deps move to root devDependencies (the originally-spec'd design) and
simplifies any future tooling that expects a conventional
node_modules. - Scope: ~5 test files (the fail-loud ones) + re-confirm the bats fail-loud case. Out of scope for J5 itself; do before/with the launcher→JS work.
The north-star packaging story: ship clode as a Node SEA (single executable
application) — clode's own logic plus an embedded Node runtime in one
self-contained binary — built in CI across an unusually broad OS/arch matrix. This
leans all the way into clode's identity: a single artifact that runs where Anthropic
ships nothing (old macOS, NetBSD/pkgsrc, non-x86, …), the run-everywhere counterpart
to upstream's Bun-compiled binary. The strategic framing lives in LONG-TERM.md
("Build a Node SEA"); this entry is the release-engineering half.
STATUS 2026-07-03: Windows SEA build Plan A SHIPPED — clode.exe builds and runs; the
toolchain is now genuinely cross-platform (validated on Windows x64 AND Linux x64).
The bats→node migration (Specs 2a/2b) shipped, so the test sources are bash-free (the
Windows trigger). "Plan A" (build + offline smoke, NO CI) — plan
docs/superpowers/plans/2026-07-03-windows-sea-build.md — is done:
platform-tag.cjs:win32 → windowsOS token + aseaBin(repo, platform)helper (clode.exeon win32).build-sea.mjs: rewritten to do the same thing on every OS rather than branch. The toolchain is now invoked via JS APIs / JS CLIs undernode— no shells, no.bin/.cmdshims, no quote-stripping:- npm runs via its own
npm-cli.jsundernode(notnpm/npm.cmd+shell). This is what let the Windows--definequotes survive and sidestepscmd.exe's UNC-cwd refusal. - esbuild and postject are called through their JS APIs (
buildSync,inject), not their CLIs. Key lesson: esbuild's publishedbin/esbuildis a NATIVE binary on POSIX but a node shim on Windows, so "run the bin under node" is portable on neither — the API is. - signing is extracted to
scripts/sea-sign.cjs <unsign|sign> <bin>, a small Node CLI that owns allcodesign/signtoolplatform branching, sobuild-sea.mjsissues one uniform call per phase. - rename-aside before write (uniform): a just-run binary can stay locked briefly
(Windows image section / AV), so
buildBinaryrenames the stale binary aside before writing — harmless on POSIX, fixes a rebuild-then-run EBUSY on Windows. - the only per-format branch left is postject's Mach-O segment name (macOS), passed as an API option.
- npm runs via its own
sea-build/sea-smoketests locate the binary viaseaBin; the POSIX-only exec-bit assertion was dropped (runnability is proven uniformly by actually executing the binary).- Validated: fresh
CLODE_SEA=1 node --test --test-concurrency=1 test/sea-build.test.cjs test/sea-smoke.test.cjsran green on Windows x64 (Node 24) and Linux x64 (Node 24) — build + offline--clode-version/--clode-helppass; bundle-boot tests skip (no provider). Use--test-concurrency=1: the two files share one build artifact and must not race (latent on POSIX, fatal on Windows). Windows build must run from a real drive path, not a raw\\wsl.localhost\…UNC path (Node's module resolution + npm choke on a UNC base) — a normalC:checkout or a drive-mapped WSL tree (net use Z: \\wsl.localhost\Ubuntu) is fine.
The question this answers: for the UNSIGNED clode.exe, who can't run it, on what basis we
believe that, how we'd learn we're wrong, and the cheapest signing path when/if it's worth paying.
Current call: ship UNSIGNED. Most Windows users run it today; revisit signing when a real
SAC-blocked user reports it (or when Windows-PE local testing on the dev box is worth ~$120/yr).
Who is affected — is the unsigned binary blocked, warned, or clean?
- ❌ Hard-blocked (no bypass): Windows 11 with Smart App Control (SAC) in Enforcement. SAC checks all executables regardless of how they arrived (download, copy, local build), so only signing fixes it — and only once reputation builds. There is no per-file "Run anyway" for SAC; the only escape is turning SAC off, which needs an OS reinstall to re-enable.
⚠️ Bypassable warning ("Run anyway"): Win10 / Win11-without-SAC when the file carries Mark-of-the-Web (browser/email download). One click, per file hash, until reputation accrues.- ✅ Runs clean, no signing needed: Windows 10 (all), Windows Server, any Win11 with SAC
Off/Evaluation, AND any Windows where the file has no MOTW — fetched via CLI (curl/wget/
gh/ winget/scoop), git, or copied. clode's dev audience skews heavily into this bucket.
Why SAC is on on the dev box (and roughly who else it hits): SAC auto-enables ONLY on a clean/OEM Win11 install (never on a Win10→11 upgrade). It starts in Evaluation, silently watches usage, then flips to Enforcement if the machine looks reputable (mostly-signed apps) or to Off if it runs lots of unsigned software. The dev box (Win11 Home, clean/OEM) passed the "looks clean" test before we started building unsigned binaries, so it locked to Enforcement; the user never chose it. So the affected group ≈ "newish clean-install Win11 users who mostly run mainstream software and got auto-enrolled" — a real, growing consumer slice, but it EXCLUDES Win10, Server, Win11-upgraded-from-10, dev/power-user machines (usually evaluated Off), and all CLI/git installs.
Basis / confidence:
- Adversarially verified (deep-research, 3-of-3 vote, primary Microsoft sources): SAC "blocks execution of unsigned files unless the file has a positive reputation" and its checks "apply to all executable files, not just those downloaded from the Internet"; the ISG decides allow/block from hash+signing info, allowing only "known good" (signing is an INPUT, not authorization); "even when signed, a newly created binary could still show a warning until its hash or publisher certificate accumulates … positive reputation" (reputation is per file-hash+publisher); EV certs no longer bypass SmartScreen ("this behavior no longer exists"); reputation builds organically from download volume over weeks, no consumer submission mechanism. Sources: learn.microsoft.com/windows/apps/package-and-deploy/smartscreen-reputation; …/application-control/…/use-appcontrol-with-intelligent-security-graph; learn.microsoft.com/azure/artifact-signing/faq; knowledge.digicert.com EV-SmartScreen alert.
- My-knowledge, NOT re-verified this run (well-established MS docs — flagged so we don't overtrust): SAC auto-enables only on clean/OEM Win11; the Evaluation→auto-on/off logic; one-way (reinstall to re-enable); SmartScreen is MOTW-gated and CLI download tools usually don't set MOTW.
- Local evidence: dev box
VerifiedAndReputablePolicyState=1; CodeIntegrity event 3033 blockedclode.exefrom LOCAL disk (so it's the signing verdict, not the WSL/UNC path); confirmed on both UNC andC:. Thewindows-latestCI runner has SAC off, which is why CI builds+smokes green.
How we'd know we're wrong (falsifiers to watch):
- Unsigned
clode.exeruns fine on a machine the user insists has SAC on → our block belief or the state read is wrong. - A plain Windows 10 / Server user reports a HARD block (not a bypassable warning) → SAC/MOTW model wrong.
- Signing ships and SAC still blocks indefinitely with reputation never accruing → the "signing eventually clears SAC" belief needs revisiting (may require a Microsoft submission).
- A CLI/git/winget install triggers SmartScreen → the "CLI = no MOTW" assumption is off for that tool.
- SAC turns itself OFF on the dev box after repeated unsigned-build blocks → contradicts "enforcement is committed/one-way."
- Still unknown: what fraction of Win11 actually ships SAC-enforcing (not established this run) — the number that would size the affected population.
Cheapest signing path when it's worth it (ranked):
- Azure Trusted Signing (Azure Artifact Signing) — ~$9.99/mo Basic (corroborated across the Azure
pricing page + 2 blogs, not formally re-verified), cloud HSM, CI-native (
azure/trusted-signing-action). Individuals in US/Canada ARE eligible (verified, Azure FAQ). Issues no EV — which no longer matters. Best default. - SSL.com eSigner — OV/IV $20/mo, EV $100/mo (verified); cloud, GitHub Action
(
sslcom/actions-codesigner). Fallback if Azure individual onboarding blocks you. - Certum Open Source — cheap for individuals BUT ships a PHYSICAL smartcard+reader → not CI-friendly; only good for local signing.
- Do NOT pay for EV to dodge SmartScreen (no longer works). Self-signed NEVER satisfies SAC.
- Reality at any price: signing gets clode evaluated instead of hard-blocked-as-unsigned, but per-(hash+publisher) reputation still ramps over weeks — no instant unlock.
scripts/sea-sign.cjsis already structured to add a realsigntool signon Windows when creds are present; wiring is a smallrelease.yml+sea-sign.cjschange behind a secret. Plan B slice 1 — Windows CI build job — SHIPPED (2026-07-04).release.yml's build matrix now includeswindows-latest(with adefaults.run.shell: bashso the existing bash steps run under Git Bash); every leg runs the offline SEA smokes (--test-concurrency=1) after the build; asset naming isseaBin-aware so the Windows asset isclode-<ver>-windows-x64.exe. Validated by a green three-OSworkflow_dispatchdraft run (run 28701118207):build (windows-latest),ubuntu-latest,macos-14all succeeded and the draft carriedclode-0.1.0-{linux-glibc2.28-x64, macos-14-arm64, windows-x64.exe}. CI is where the Windows PE is validated (the runner has no Smart App Control, unlike the dev box). Bug the CI pass caught + fixed:stageDepsshelled out totarwith aD:\…\deps.tarpath, and undershell: bashtaris Git Bash's GNU tar, which read the drive-letter colon as a remotehost:path("Cannot connect to D:"); fixed to archive viatar -cf -(stdout) with the staging dir as cwd — no colon-bearing path args, uniform on GNU tar and bsdtar (commit 28be5c0).
Plan B slice 2 — real bundle boot on Windows — SUCCEEDED (2026-07-04, spike on branch
spike/windows-boot, findings in docs/superpowers/findings/2026-07-04-windows-boot-spike.md).
clode.exe --version boots the real Claude Code bundle on windows-latest CI → prints
2.1.201 (Claude Code), and sea-smoke.test.cjs's "boots the real bundle" + "reuses sea-deps"
tests pass there (4 pass / 1 skip). Unknown #1 answered YES: npm i -g @anthropic-ai/claude-code
delivers a real Bun-compiled claude.exe (~241MB); clode's byte-scan bundle-carve finds its
@bun-cjs blocks and extracts JS from the PE with ZERO extractor changes. The ONLY runtime code
fix needed was the clode-sea.cjs tar -xf → stdin+cwd change (commit 3f8b558, mirrors the
build-side fix). Provider on Windows comes via npm, not downloads.claude.ai (no Windows
artifact — all platform keys 404).
Plan B slice 3 — permanent per-commit SEA CI gate — SHIPPED (2026-07-04, commit f87dc87).
.github/workflows/ci.yml runs on every push to main + every PR, matrix ubuntu/macOS/Windows:
build clode.exe → offline SEA smoke → npm-install the Claude provider → REQUIRED real bundle
boot (one node --test sea-build+sea-smoke run, CLODE_CLAUDE_BIN set, -p skipped offline).
Validated by a green 3-OS PR (#10) then a green main run: boot ran and passed on all three
(ubuntu found the nested claude-code-linux-x64/claude, macOS+Windows the claude-code/bin/ claude.exe; clode's byte-scan extractor carves PE or ELF). Provider located via npm root -g
(portable) — NOT npm prefix -g/node_modules, which is wrong on POSIX. This is the first-ever
bundle-boot coverage on any platform in CI, and the runtime-path half of the "don't silently break
a platform" safety net. release.yml untouched (standalone ci.yml). The windows-boot spike
workflow + branch are retired.
Plan B slice 4 — Windows interactive TUI/session bringup — SUCCEEDED (2026-07-04, local
human-in-the-loop spike on the SAC-disabled dev box; findings in
docs/superpowers/findings/2026-07-04-windows-tui-bringup.md). With SAC turned off
(VerifiedAndReputablePolicyState=0x0, the decided dev-box trade-off) the unsigned clode.exe built
from main runs a fully working native-Windows Claude Code session on a Pro/Max subscription
(no API key): clode.exe --version → 2.1.201; the Ink/React TUI renders + accepts input + runs
a turn (user-driven); -p "reply PONG" round-trips through the subscription; file + directory
tools work. Provider isolated via CLODE_CLAUDE_BIN (resolution stays deferred). Zero code
changes needed — every probe passed on current main. Non-regression: WSL Linux suite unchanged
(the only 2 fails are the pre-existing ugrep-absent grep shadow env tests, unrelated). Benign
cosmetic-only noise: the -p "no stdin data received in 3s" launcher warning.
Unknown #2 — the POSIX shell-shim / tool-use path — CHARACTERIZED: WORKS on Windows (2026-07-04).
The make-or-break probe (-p "Use the Bash tool to run exactly: echo HELLO123") returned HELLO123,
exit 0 — bun-shim.cjs's entirely-POSIX shell machinery ($SHELL -c -l / exec -a / argv0-shadow /
shell-snapshot) functions natively on Windows through the real Bun claude.exe provider. The
spike's central hypothesis (that it would break and need a porting slice) was wrong; no shell-shim
port is required to reach basic tool use. (Earlier this line read "UNTESTED; only a real -p
session (needs an API key) would exercise it" — resolved: a Pro/Max subscription -p session
exercised it and it works.)
Plan B remaining:
- code-signing (only matters for SAC-Enforcement Win11 users — see the "Code-signing & Smart App Control" decision memo above; deferred until a real SAC-blocked user reports it).
- Windows provider resolution — native-installer layout DONE (2026-07-04, commits
e69f753+4cd28cf; findingsdocs/superpowers/findings/2026-07-04-windows-provider-resolution.md).clode.exe --versionnow boots on native Windows with noCLODE_CLAUDE_BIN: resolver step 5 derives HOME viaclode-paths.homeDir(os.homedir()fallback) and tries leaf namesclaude/claude.exe, so it finds the native installer's~/.local/bin/claude.exe(a plain copy; POSIX symlinks, Windows copies). Uniform, no platform branch. Still deferred:- npm
$basedirsh-shim (followWrappercan't follow the quoted$basedir-relative exec target; npm is now upstream-de-emphasized "Advanced" —CLODE_CLAUDE_BINbridges meanwhile); - WinGet layout;
- step 3's
providers/currentjoin (hardcodes/claude+ POSIX separator) → belongs to the provider fetch/update slice below.
- npm
- Windows provider fetch/update — DONE (2026-07-05, commits
0a3e4e3..afccf9f; findingsdocs/superpowers/findings/2026-07-05-windows-provider-fetch-update.md).clode updateworks on native Windows:providers/currentis now a pointer file (a tiny text file with the version) behind the singleclode-current.cjsseam — no symlink, no privilege needed; uniform on every platform; the path still contains/providers/<ver>/so the shared-per-version cache key holds. The downloaded platform is a fixed arbitrary container (linux-x64) that clode carves the JS out of (never runs); the download URL uses the manifest's self-describingbinaryname while the store keeps the canonicalclaude. Verified live on Windows:clode updatefetched linux-x64 2.1.201, wrote a pointer-filecurrent, andclode.exe --version(noCLODE_CLAUDE_BIN) resolved via step 3 through the pointer and booted. Characterized win32 vs linux carves: same program, byte differences are only cosmetic (minifier names, embedded-doc CRLF-vs-LF, Bun VFS prefix); linux == macOS byte-for-byte. (The earlier "no Windows artifact" note was the wrong endpoint —downloads.claude.ai/claude-code-releases/<ver>/<plat>/serves every platform,binary=claude.exefor win32.) Legacy symlinkcurrentself-heals on next update. Still deferred: auto-fetch on missing provider, WinGet, npm$basedirsh-shim resolution. - full
node --test(non-SEA) suite green on Windows — being done in cause-based slices:- Slice 1 — cross-platform runner + path/format — DONE (2026-07-05, commits
5fe5457..8cb4952; findingsdocs/superpowers/findings/2026-07-05-windows-test-runner.md).test/run.mjs(a node orchestrator) replaces the bashrun-all.sh—npm test→node test/run.mjs, identical on every OS (CLODE_NODE=execPath, platform-tagged NODE_PATH viapath.delimiter, PTY-harness install vianpm-cli.jsunder node, hermetic guardrequire()d in-process around fs-discoveredtest/*.test.cjs, dotfiles excluded to match the POSIX glob).harness-preflightsmoke spawns node not/bin/sh. Path/format fixed:cacheKeymatches markers on either separator; resolve step 5 usespath.join;platformTagregex +clode-paths/clode-watchasserts tolerate backslashes. Verified: node-pty 1.1.0 installs prebuilt on Windows + ConPTY spawns headlessly; POSIX suite unchanged (378/337/fail 2 ugrep-baseline/39); Windows now RUNS the whole suite — pass 300 / fail 35 / skip 43, with all the path/format tests green. No skips added. The remaining 35 Windows failures are the honestly-deferred buckets below. - Slice 2 — symlink fixtures — DONE (2026-07-05, commits
59faed4..8e4a26a; findingsdocs/superpowers/findings/2026-07-05-windows-symlink-fixtures.md). No skips: the two incidental symlink fixtures (e2e-resolve~/.local/bin/claude, clode-watch real-semver) becamefs.cpSynccopies (uniform, privilege-free); the two symlink-semantics tests (resolve5/5b) were rewritten to inject a mockfsmthat simulates the symlink, testing clode's readlink+anchor logic on every platform without a real OS symlink. Windows EPERM-symlink failures 18 → 2 (the 2 are the deferred PTYmakeWsWorldsnode symlink); Windows suite fail 35 → 28; POSIX unchanged (378/337/fail 2/39). Newly surfaced (was masked by the buildFixtures EPERM crash): the e2eclaude on PATH lasttest fails on Windows for a non-symlink reason —whichClaude/PATH resolution + exec of a bareclaudefixture file on Windows (no.exe, X_OK semantics). Folded into the resolution/PATH work below, not symlinks. ThefollowWrapperPOSIX exec-wrapper tests (~4) remain — assess POSIX-only vs npm-shim. - Windows PATH provider (
whichClaude) +claude-on-PATH exec: step 6 (claudeon PATH) uses a bareclaudename +accessSync(X_OK); on Windows a PATH provider isclaude.exe/claude.cmdand X_OK differs. Surfaced by the e2eclaude on PATH lastfailure. Small; pairs with the npm-shim slice. - Slices 3–5 (combined) — mostly DONE (2026-07-05, commits
d323459..b515e09; findingsdocs/superpowers/findings/2026-07-05-windows-suite-remainder.md). Principle: fix portability bugs; mock POSIX-shell/tool LOGIC uniformly (no gratuitous skips); capability-gate only real-shell/tool integration. POSIX is now fully green (fail 0) — the 2 long-standingugrep/bfsbaseline fails are fixed (the argv0-shadow tests source underbash, not dash, which couldn't parsefunction name{}). Windows failures 28 → 9. Landed:followWrapperpath.win32.isAbsolute;e2e-distnode file-scan (nogrep);runBundlesignal-integration gated to POSIX; snapshot-rewrite real-shintegration gated (logic covered by pure-string tests); inspect applet-version injectable-spawn mock;clode-depsfake-npm injectable-spawn mock;whichClaudePATHEXT +6bmock;writeUpdateGuardSettingsJSON.stringify escapeclode_updateexec-bit POSIX-gate;makeWsWorldsnode copy (PTY/TUI now 2 pass/0 fail/4 live-render-skip on Windows — the PTY "wildcard" was just the symlink).
- Windows suite — final 9 residuals — DONE (2026-07-05). Suite is now GREEN on both platforms:
Windows
pass 329 / fail 0 / skip 50; Linux (WSL)pass 340 / fail 0 / skip 39(the 11-skip delta is the Windows-gated set). What landed:warnAppletSkew×3 (snapshot-rewrite.test.cjs): madewarnAppletSkew(shadows, spawn=_rawSpawnSync)accept an injectable spawn (mirrorshostAppletVersion); the tests inject amockSpawn(code, stderr)instead of writing a#!/bin/shstub (unrunnable on Windows).writeUpdateGuardSettingstest: dropped the byte-for-byte assertion (the runtimeJSON.stringifyescapes backslashes on Windows) — theJSON.parse+command-field checks already present cover it.rewritten grep shadow×2 (e2e-argv0_shadow.test.cjs): gatedBASH_SKIPto also skip onwin32(POSIX-shell-under-bash with POSIX paths / stub applets has no Windows analog; logic covered by the pure-stringrewriteSnapshottests).e2e claude on PATH last: the actual root cause was NOT the leaf name (X_OK is ignored on Windows so a bareclaudefile resolves fine) but the test hardcoding:as the PATH separator — split onpath.delimiter(;on Windows) mashedpathDirinto the next entry. Fixed both PATH joins ine2e-resolve.test.cjsto usepath.delimiter. (Test 3 only passed before because it resolves via step 5 first.)e2e-deps×2 (auto-install,changed manifest) — the BACKLOG's "clode-deps state quirk" guess was wrong: these are the launcher-levele2e-deps.test.cjstests (same test names as the unit file), and they fail becausefakeNpmemitted a#!/bin/shshim clode can't spawn on Windows. Root cause is a real product bug: clode-deps spawnsCLODE_NPMdirectly, and Windows Node rejects directspawnSyncof a.cmd/.batwithEINVAL(CVE-2024-27980 hardening) — so realnpm.cmdwould fail too. Fixed in product code (clode-deps.cjsnpmInvocation): on win32 a.cmd/.batnpm is routed throughcmd.exe /d /s /c(the Windows parallel of the sh launcher invoking npm); POSIX /.exespawn verbatim. MadefakeNpmcross-platform (a.cmd+paired.cjs, node invoked by absolute path since the sandbox PATH carries no node) and gave the sandbox aComSpecon win32 (the OS-shell constant, parallel to/bin/shbeing reachable via the POSIX sandbox PATH). The e2e now genuinely drives npm to completion on Windows — closing a realnpm.cmd-on-Windows capability gap, not just the test.
- Next: add a Windows CI leg to
ci.yml(now genuinely green, 0 residuals).
- Slice 1 — cross-platform runner + path/format — DONE (2026-07-05, commits
- Windows (and Linux/macOS) arm64 — the npm registry ships
*-arm64Bun providers; the SEA build matrix is x64/darwin-arm64 only. (macOS SEA build+smoke+boot is now continuously verified every commit byci.yml, so the old "re-verify on a Mac someday" caveat is retired.)
STATUS 2026-06-30: the JS-port prerequisites are DONE — SEA is now the next actionable frontier.
clode is fully JS+sh→JS: the extractor/inspector/signals are .cjs, the inline
snippets are node, the test harness is JS, AND bin/clode is now a
#!/usr/bin/env node launcher (the sh is deleted; libexec/clode-main.cjs +
clode-*.cjs modules; bundle run via spawn-child+signal-forward). So "Port the
toolchain to JS" below is COMPLETE. Remaining SEA work is the release-engineering:
vendor the runtime ext-deps into the blob, per-target postject injection + Node
build matrix, pin the embedded Node ahead of the bundle floor, replace curl/wget
(already done — clode-net uses fetch). SEQUENCING (user-set): Node SEA for
Mac+Linux first; convert bats→Node only when aiming at Windows (bats won't run
there); then Windows SEA; QuickJS the further frontier.
- What it unlocks: a zero-dependency install —
curlone binary and run. No host Node (clode carries its own), no Python, no npm, no curl/wget. Reaches hosts that have no Node, or one too old for the bundle's creeping Node floor. - Prerequisites (the real work):
- Port the toolchain to JS. Node SEA packs JS+Node only, so the Python
extractor (
libexec/extract-claude-js) and the shell launcher (bin/clode) have to be reimplemented in JS to be SEA-packable. Drops the Python dependency as a side effect. This is the big one and gates everything else. (The extractor half is broken out above as "Reimplement the extraction toolchain in JavaScript" — worth doing on its own, ahead of SEA.) - Vendor runtime deps (
ws,yaml,string-width,strip-ansi,wrap-ansi,semver) into the SEA blob/assets instead ofensure_deps' npm-install-on-first-run. - Per-target injection. SEA blob injection (
postject) runs against a target Node binary and doesn't cross-compile freely, so the matrix needs a runner or cross toolchain per target. Exotic targets (NetBSD, legacy macOS, non-x86) may have no official Node build — supplying custom Node builds for them is precisely the "matrix the likes of which the world has never seen" flex. - Pin + track the embedded Node against the bundle's required major (today
= 24, creeping); the SEA's Node must stay ahead of it.
- Replace curl/wget in the update path with Node's built-in
fetchso the last external tool dependency drops too.
- Port the toolchain to JS. Node SEA packs JS+Node only, so the Python
extractor (
- Keeps the runtime model: the SEA still fetches upstream JS into the provider
store and extracts+runs it under the embedded Node;
clode update/ re-extract-on-change are unchanged. The SEA changes how clode itself is shipped, not how it runs Claude Code. - Gating: "once everything's working really well" (per
LONG-TERM.md) — the shim coverage, the extract-time durability gate, and the JS port want to be solid first. - Next step beyond SEA — QuickJS for true any-platform native builds. Node SEA
still needs a Node binary per target, so the reachable set is bounded by where Node
itself builds. The further reach is to implement enough of the Node API surface that
the app (clode's own JS toolchain, once ported, plus the extracted bundle) runs under
QuickJS, which compiles to a native binary essentially anywhere — no per-target
Node required. The cost shifts to us: with QuickJS, providing the Node APIs the
bundle + toolchain use becomes our job (the same kind of host-API gap-filling the
bun-shimalready does forBun.*, one layer down). Sequencing: JS toolchain port → Node SEA → QuickJS. Strategic framing + a Node-compat-layer pointer live inLONG-TERM.md("Build a Node SEA"). Phase 1 (technology qualification) ran 2026-07-05/06: GO — build the phase-2 node-compat port on quickjs-ng (engine) + txiki.js (IO layer); 27 of 62 Node-API keys are provided natively, 30 shimmable, 4 need scoped C, 1 hard (degrades to a stub), 0 architectural blockers, and the North-Star bytecode memory path measured on darwin/aarch64 fits the mac68k-class ceiling (mac68k itself unmeasured, qemu boot gap) — seedocs/superpowers/specs/2026-07-05-universal-binaries-phase1-report.md.
The release-matrix s390x-musl leg is the FIRST time the node-shim suite runs against a Linux tjs (the pinned binary is darwin; the qemu guests are NetBSD). The BE oracle (run 29065977866) proved the engine is big-endian-clean (url characterization + crypto KAT + Buffer semantics all green), but surfaced darwin assumptions baked into the shim that a Linux target exposes. These are Linux-portability debt, NOT big-endian bugs — their own TDD workstream, and they'll matter for the eventual Linux-NATIVE (non-emulated) legs too:
- os.constants.signals is a hardcoded darwin table. The shim returns darwin signal numbers (SIGBUS=10) where Linux differs (SIGBUS=7); host-node diff fails on Linux. Fix: per-platform signal tables (linux / darwin / netbsd / the BSDs) in libexec/node-shim/modules/os.cjs (or constants.cjs). TDD: characterize against host node per-platform.
- bun:ffi
suffixhardcodes the macOS 'dylib'. Should be platform-aware (.so on Linux + the BSDs, .dylib on darwin, .dll on Windows) — libexec/bun-shim.cjs BUN_BUILTINS['bun:ffi'].suffix. The bunshim test also hardcodes the macOS extension; fix both. - Audit for other hardcoded darwin assumptions now that a Linux target exists: library extensions, default paths, any os.constants.* / process.* / signal / errno tables, DYLD_* vs LD_* env handling. Sweep libexec/node-shim/** and libexec/bun-shim.cjs.
Not matrix-blocking (the BE oracle scopes these out and is non-blocking); the published Linux builders are musl-static and smoke green (PONG + attest). This is about node-shim FIDELITY on Linux, which the shim-fidelity gate should grow to cover once a Linux tjs is a first-class local build target.