You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bad --settings path: node still writes the session transcript, perry writes none (E36_badsettings_p) #9575
From the full verification sweep at ed99c35cd (case E36_badsettings_p: -p hi with a malformed --settings file):
node rc=[1] | perry rc=[1]
ISSUES: HOME-PATHS-ONLY-IN-NODE: ['.claude/projects/<CWDKEY>/<UUID>.jsonl']
Exit codes and output match — but on the settings-error path node still records the session transcript and perry records nothing. Same family as the #9421 root cause (an error path where the transcript machinery is skipped or its write dies before the directory exists), but on the settings-error branch rather than --bare. Since #9491 made fs errors throw, check first whether perry's writer runs and throws here (swallowed higher up) or is never reached.
Also from the same sweep, one persisting item to re-check against #9487's closure: B60_install_badtarget still exits 0 under perry where node exits 1 — either the fix missed the install subpath or a train dropped a hunk; whoever takes this issue can verify both in one session.
Repro: harness case E36_badsettings_p in /root/claude-ccstress/; tip binary /root/claude-roots/app_c08.
I’m working on this now. I reproduced E36_badsettings_p three times against the referenced app_c08 binary: both engines exit 1 every run, while only Node writes .claude/projects//.jsonl. I’ll trace whether Perry skips the write or loses it on the error path, add a minimized regression where possible, and re-check B60 separately.
Re-ran the exact case against a freshly built bundle at current main (0e3ce3812), with /root/claude-roots/app_c08 (8d27f7b96, the tip binary this issue cites) as a paired control in the same environment. The main bundle carries a single runtime stamp (PERRY_RUNTIME_BUILD_STAMP_V1|…|build=git:0e3ce3812…) — my first attempt was a Frankenstein (the worktree's target/ was a symlink into a tree checked out at the old commit, so --enable-wasm-runtime rebuilt the old runtime under a new compiler); that build was discarded and redone in an isolated worktree.
Harness, REPS=3, three independent rounds (9 runs per engine):
~/.claude/settings.json = {} (user level, no project settings)
✗
✓
The discriminator is narrower than "malformed JSON"
It is not JSON parsing and it is not project-specific: it is a settings file existing at all, at either level, whatever the contents — a 0-byte file triggers it too. What those cases share is that claude-code starts its settings-file watcher; cc's own debug log (CLAUDE_CODE_DEBUG_LOGS_DIR) makes it exact — Watching for changes in setting files … appears in precisely the failing configurations and never in the passing ones. An empty .claude/ directory does not start it, and the transcript survives.
What was actually going wrong
cc's shutdown (WK()) awaits its whole cleanup registry — await Promise.all(Array.from(z11).map((q) => q())) — and both the session-transcript flush (eq(async () => { await writer.flush(); … })) and the run_state_at_shutdown diagnostic are entries in it.
On app_c08 with a settings file present, that chain did not complete. CLAUDE_CODE_DIAGNOSTICS_FILE shows the earlier exited cleanup firing but run_state_at_shutdown missing, and strace shows the transcript opened once O_RDONLY (ENOENT) and then never for writing — exit_group(1) lands 1.6 ms after the final stdout write, with no O_WRONLY|O_CREAT|O_APPEND at all.
That it is the cleanup chain and not the 100 ms flush timer losing a race is confirmable from the outside: give cc a SessionEnd hook, and the hook's subprocess keeps the process alive long enough for the timer to fire — the transcript reappears while run_state_at_shutdown is still missing. So the flush cleanup was not running; the timer was the only thing that ever wrote it.
On current main both symptoms clear together: run_state_at_shutdown is present in all four settings placements, and the transcript append lands inside shutdown — 8.7 ms after the final output and 1.3 ms before exit_group. That is the chain completing, not a timing shift.
Attribution
Not bisected commit-by-commit (each point is a ~35 min bundle compile on a contended host), but the syscall evidence points at one commit. The failing configuration is exactly "cc creates an fs.watch-backed (chokidar) settings watcher", whose teardown W() is a synchronous entry in that same cleanup registry, and the watcher backend demonstrably changed under it:
app_c08 (8d27f7b96), settings present: inotify_add_watch = 0 — the old ref'd 25 ms setInterval tree re-walk.
main (0e3ce3812), settings present: inotify_add_watch = 4, on <cwd>/.claude and <cwd>/.claude/settings.json. Settings absent: 0, on both binaries.
fd17689ca6 (#9591, changelog.d/9613-fs-watch-os-events.md — "fs.watch uses OS change notifications, not a 25 ms tree re-walk … liveness moved from the ref'd timer to a runtime has-active slot") is the only commit in 8d27f7b96..main that touches fs.watch, and it is the change those syscalls show. So: incidentally fixed there, on the balance of the evidence rather than by bisect.
Closing — the transcript is written in every settings shape on current main.
B60_install_badtarget — still failing, and it is a different bug
Re-checked as asked; unchanged on current main: node rc=1, perry rc=0, perry's stdout one byte short (the trailing \n).
Root cause found, and it is not the install path, so #9487's closure did not miss a hunk. cc's install subcommand is
— the callback closes over O, the binding introduced by the very declarator whose initializer creates it. Under perry that callback throws ReferenceError: O is not defined (cc logs uncaught_exception, 3/3 runs), and the uncaught throw is what loses the last write and the exit code.
Minimized and reproducing on current main:
const{u: O}=awaitmk({cb: ()=>{O();}});// perry: ReferenceError: O is not defined
Every shape where the closure is built inside the declarator's initializer fails — object destructuring, array destructuring, plain binding, with and without await, let and const; a closure created after the declaration is fine.
Filed as #9718 with the minimized repro and the shape matrix; a fix is in progress there. Not holding this issue open for it.
Reproduction artifacts
On the build host: harness copy /root/cc9575/ (harness.py, compare.py, cases_9575.jsonl, probe*.sh), main bundle /root/cc9575/app_main, isolated worktrees /root/cc9575/wt-main and /root/cc9575/wt-old.
Closing: E36_badsettings_p passes on current main (3 rounds x REPS=3, and 10/10 on the direct probe), with app_c08 as a paired control failing 0/10 in the same environment. B60 is tracked separately as #9718.
claude install not-a-version now exits 1 with 161 bytes of stdout and no uncaught_exception, matching node exactly (unpatched main: rc 0, 160 bytes, one ReferenceError: O is not defined).
Activity
proggeramlug commentedon Sep 4, 2026
I’m working on this now. I reproduced E36_badsettings_p three times against the referenced app_c08 binary: both engines exit 1 every run, while only Node writes .claude/projects//.jsonl. I’ll trace whether Perry skips the write or loses it on the error path, add a minimized regression where possible, and re-check B60 separately.
proggeramlug commentedon Sep 4, 2026
E36_badsettings_p— fixed on currentmainRe-ran the exact case against a freshly built bundle at current
main(0e3ce3812), with/root/claude-roots/app_c08(8d27f7b96, the tip binary this issue cites) as a paired control in the same environment. The main bundle carries a single runtime stamp (PERRY_RUNTIME_BUILD_STAMP_V1|…|build=git:0e3ce3812…) — my first attempt was a Frankenstein (the worktree'starget/was a symlink into a tree checked out at the old commit, so--enable-wasm-runtimerebuilt the old runtime under a new compiler); that build was discarded and redone in an isolated worktree.Harness,
REPS=3, three independent rounds (9 runs per engine):app_c08(8d27f7b96)FAIL … HOME-PATHS-ONLY-IN-NODE: ['.claude/projects/<CWDKEY>/<UUID>.jsonl']main(0e3ce3812)PASS,PASS,PASSDirect probes (fresh
HOME,-p hi, stdin closed) — is the transcript written?app_c08main.claude/dir, or an unrelated file in.claude/.claude/settings.json={}.claude/settings.json= malformed /5/null/{"model":"x"}/ whitespace.claude/settings.json, 0 bytes.claude/settings.local.json={}~/.claude/settings.json={}(user level, no project settings)The discriminator is narrower than "malformed JSON"
It is not JSON parsing and it is not project-specific: it is a settings file existing at all, at either level, whatever the contents — a 0-byte file triggers it too. What those cases share is that claude-code starts its settings-file watcher; cc's own debug log (
CLAUDE_CODE_DEBUG_LOGS_DIR) makes it exact —Watching for changes in setting files …appears in precisely the failing configurations and never in the passing ones. An empty.claude/directory does not start it, and the transcript survives.What was actually going wrong
cc's shutdown (
WK()) awaits its whole cleanup registry —await Promise.all(Array.from(z11).map((q) => q()))— and both the session-transcript flush (eq(async () => { await writer.flush(); … })) and therun_state_at_shutdowndiagnostic are entries in it.On
app_c08with a settings file present, that chain did not complete.CLAUDE_CODE_DIAGNOSTICS_FILEshows the earlierexitedcleanup firing butrun_state_at_shutdownmissing, andstraceshows the transcript opened onceO_RDONLY(ENOENT) and then never for writing —exit_group(1)lands 1.6 ms after the final stdout write, with noO_WRONLY|O_CREAT|O_APPENDat all.That it is the cleanup chain and not the 100 ms flush timer losing a race is confirmable from the outside: give cc a
SessionEndhook, and the hook's subprocess keeps the process alive long enough for the timer to fire — the transcript reappears whilerun_state_at_shutdownis still missing. So the flush cleanup was not running; the timer was the only thing that ever wrote it.On current
mainboth symptoms clear together:run_state_at_shutdownis present in all four settings placements, and the transcript append lands inside shutdown — 8.7 ms after the final output and 1.3 ms beforeexit_group. That is the chain completing, not a timing shift.Attribution
Not bisected commit-by-commit (each point is a ~35 min bundle compile on a contended host), but the syscall evidence points at one commit. The failing configuration is exactly "cc creates an
fs.watch-backed (chokidar) settings watcher", whose teardownW()is a synchronous entry in that same cleanup registry, and the watcher backend demonstrably changed under it:app_c08(8d27f7b96), settings present:inotify_add_watch= 0 — the old ref'd 25 mssetIntervaltree re-walk.main(0e3ce3812), settings present:inotify_add_watch= 4, on<cwd>/.claudeand<cwd>/.claude/settings.json. Settings absent: 0, on both binaries.fd17689ca6(#9591,changelog.d/9613-fs-watch-os-events.md— "fs.watch uses OS change notifications, not a 25 ms tree re-walk … liveness moved from the ref'd timer to a runtime has-active slot") is the only commit in8d27f7b96..mainthat touchesfs.watch, and it is the change those syscalls show. So: incidentally fixed there, on the balance of the evidence rather than by bisect.Closing — the transcript is written in every settings shape on current
main.B60_install_badtarget— still failing, and it is a different bugRe-checked as asked; unchanged on current
main: noderc=1, perryrc=0, perry's stdout one byte short (the trailing\n).Root cause found, and it is not the
installpath, so #9487's closure did not miss a hunk. cc'sinstallsubcommand is— the callback closes over
O, the binding introduced by the very declarator whose initializer creates it. Under perry that callback throwsReferenceError: O is not defined(cc logsuncaught_exception, 3/3 runs), and the uncaught throw is what loses the last write and the exit code.Minimized and reproducing on current
main:Every shape where the closure is built inside the declarator's initializer fails — object destructuring, array destructuring, plain binding, with and without
await,letandconst; a closure created after the declaration is fine.Filed as #9718 with the minimized repro and the shape matrix; a fix is in progress there. Not holding this issue open for it.
Reproduction artifacts
On the build host: harness copy
/root/cc9575/(harness.py,compare.py,cases_9575.jsonl,probe*.sh), main bundle/root/cc9575/app_main, isolated worktrees/root/cc9575/wt-mainand/root/cc9575/wt-old.proggeramlug commentedon Sep 4, 2026
Closing:
E36_badsettings_ppasses on current main (3 rounds x REPS=3, and 10/10 on the direct probe), with app_c08 as a paired control failing 0/10 in the same environment. B60 is tracked separately as #9718.proggeramlug commentedon Sep 4, 2026
Follow-up:
B60_install_badtargetis now fixed too, by #9720 (the closure/initializer binding bug filed as #9718).Rebuilt
cli_2.1.112.jswith the patched compiler and re-ran the harness for both cases this issue carried:claude install not-a-versionnow exits 1 with 161 bytes of stdout and nouncaught_exception, matching node exactly (unpatched main: rc 0, 160 bytes, oneReferenceError: O is not defined).fix(hir): let a declarator's own initializer forward-capture its binding