Skip to content

fix(panel): refresh a stale hook script so handoffs record - #144

Merged
StuBehan merged 1 commit into
mainfrom
fix/notify-script-drift
Jul 29, 2026
Merged

fix(panel): refresh a stale hook script so handoffs record#144
StuBehan merged 1 commit into
mainfrom
fix/notify-script-drift

Conversation

@StuBehan

Copy link
Copy Markdown
Collaborator

A user reported an empty Tickets tab. ~/.stack-nudge/handoffs.jsonl didn't exist, so captureHandoff had never completed once, and grep -c claude_session_id ~/.stack-nudge/notify.sh returned 0.

Their installed hook script predated v1.12.0, the release that added claude_session_id to the socket payload, while the app was on 1.25.3. notify.sh is half of the wire protocol with the panel, but it lives in ~/.stack-nudge/ and is only written by install.sh or the first-launch wizard, which is skipped once the file exists. Updates swap the .app alone. So an install that has only ever self-updated keeps whatever script version first arrived, forever, and every Stop event hits a silent return. Notifications keep working, so only the Tickets tab looks broken.

Changes

The payload no longer depends on jq. session_id / transcript_path were extracted with jq and degraded to empty strings when it was absent, which is indistinguishable from "no session". They're now parsed by the python3 block that already builds and sends the payload. The hook JSON reaches it via env, capped at 32 KiB: env and argv share ARG_MAX, and a PermissionRequest tool_input can carry a whole file, which would fail the exec and lose the event.

The installed script self-heals. notify.sh carries # stack-nudge-version: <x.y.z>, registered in .release-please-config.json extra-files beside panel/Info.plist. Bootstrap.refreshNotifyScriptIfNeeded() runs from applicationDidFinishLaunching and rewrites the installed copy when its stamp differs from the bundled one. Stamp comparison rather than content hashing, so a script deliberately tweaked at the current version survives and a version bump is the overwrite signal.

The swap is temp file + replaceItemAt, because agents are live while the app runs and a remove-then-copy window would leave a hook with no script to invoke; install.sh does the same with mv -f. chmod happens after the replace: replaceItemAt deliberately carries the original file's attributes onto the replacement, so a copy that arrived 0644 would otherwise stay non-executable and every hook would fail with "permission denied". A test pins that.

The write path is observable. captureHandoff's silent returns now count by HandoffDropReason (missing session id / missing cwd / not a git repo) and log a line to app.log, except the not-a-repo case, which is an expected skip. OutcomesView.emptyReason picks the empty state from that state instead of always claiming no sessions: filtered by hide-shipped, all dropped with the remedy, or genuinely idle.

Surfacing. Settings' about footer warns when a stamp mismatch survives the launch repair, which means the rewrite itself failed (read-only dotdir, wrong owner). install.sh notes a missing jq as optional, now that only the permission-banner detail uses it. README documents the empty-tab triage and the refresh.

Verification

Check Result
jq-free payload, real jq-withheld PATH main: claude_session_id: <MISSING>; this branch: present
24 assertions via an in-module @main harness pass
make typecheck-tests (27 files) clean
./build.sh, bash -n, shellcheck --severity=warning clean

swift test needs XCTest (Xcode-only), so the assertions were run through an in-module harness locally and CI is the authority on the XCTest suite. The shim gained XCTAssertThrowsError and addTeardownBlock.

Not verified live: the launch-time refresh inside a running bundle, which needs make reload to replace the installed app.

Out of scope

No backfill: nothing was ever recorded for affected users, and reconstructing a ledger from transcripts is separate work. permission_context() still uses jq.

@StuBehan
StuBehan force-pushed the fix/notify-script-drift branch from ab9d060 to 3f6f547 Compare July 29, 2026 18:13
@StuBehan
StuBehan merged commit 20ad229 into main Jul 29, 2026
6 checks passed
@StuBehan
StuBehan deleted the fix/notify-script-drift branch July 29, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant