Skip to content

Keep the hook, the login item and the app's deck working across upgrades and opt-outs - #1942

Merged
BarganConstantin merged 6 commits into
developmentfrom
fix/install-and-env
Oct 5, 2026
Merged

BarganConstantin merged 6 commits into
developmentfrom
fix/install-and-env

Conversation

@BarganConstantin

Copy link
Copy Markdown
Owner

What changes

  • The hook command and the login item name a node that survives an upgrade. process.execPath has its links resolved, so a Homebrew or linked-tarball node put a versioned path (…/Cellar/node/<version>/bin/node, …/node-v24.21.0-linux-x64/bin/node) into settings.json and the login job. The next upgrade deleted it, so every hook exited 127 and Claude Code showed a hook error on every tool call. Both now use the first PATH entry that links to the running node, or Homebrew's opt/<formula>/bin/node, and fall back to execPath when neither exists (new src/server/stable-node.mjs). The desktop app's CCDECK_HOOK_RUNTIME override still takes precedence.
  • The login item carries the installing shell's opt-outs. It used to carry only CLAUDE_CONFIG_DIR / CCDECK_HOME / CODEX_HOME, so a deck started at login ran without AGENTS_DECK_NO_REPORTS, AGENTS_DECK_NO_LAN and the other settings. Every variable in README's environment table is now carried by name (SETTING_VARS), only when it is set, and nothing else from the shell goes in. A test checks the list against README's table.
  • The desktop app's deck gets the documented variables the login shell sets. The app read only PATH from the login shell. It now asks the shell for PATH plus the variables in the same table (by name), puts any it lacks into its own environment before the first deck lookup (so findDecks and writeLauncher use the right CLAUDE_CONFIG_DIR), and passes them to the deck it starts. If the app was launched with a value already set, that value wins.
  • pnpm dlx, bunx and yarn dlx runs count as one-off copies, like npx. Before, they got a login item pointing into their cache or temp folder. They also got an in-app npm i -g that the away-update repeated every 30 minutes without changing the running version. Now there is no login item, the upgrade is refused with a new one_off reason (mode null), and the banner shows npm i -g ccdeck@latest as a command to run.
  • No login item on a Linux machine that doesn't run systemd. installService checks /run/systemd/system before writing anything. The first start used to say "will now start when you log in" and save the item as installed. Now it reports that it couldn't set one up.
  • The systemd unit escapes % everywhere. StandardOutput= / StandardError= and ExecStart now escape % as %%. ExecStart also escapes $ as $$, and quotes any argument containing a quote or backslash, using systemd's escapes.
  • README: the login item paragraph, the one-deck rule, the updating table and the desktop app section now match the new behaviour.

Verification

  • npm run typecheck: clean.
  • Full suite: 903 files, 11797 tests passed.
  • New regression tests. Each was run before its fix and failed for the reason described:
    • hook-command-stable-node.test.ts: settings.json named '/home/…/node-v24.21.0-linux-x64/bin/node' where the PATH link was expected. The login job did the same. A simulated upgrade (repoint current, delete the old version) leaves the new path working.
    • login-item-opt-outs.test.ts: the plist and the unit had no AGENTS_DECK_NO_REPORTS. Now checks that every variable in README's table is carried and that unrelated variables are not.
    • desktop-shell-env.test.ts: a real /bin/sh stands in for $SHELL, with a profile that exports the opt-outs. Before the fix only PATH reached the deck (checked first through the old shellPath + startDeck path). The block is registered in skip-gates.mjs, with the Windows counts in the inventory test and publish.yml updated.
    • one-off-runner.test.ts: upgradeBlock returned null (mode "install") for pnpm dlx, older pnpm dlx, bunx and yarn dlx layouts.
    • login-item-no-systemd.test.ts: installService returned ok: true, how: "file-only" and wrote the unit when systemctl was missing.
    • systemd-unit-escaping.test.ts: StandardOutput= and ExecStart kept a bare %. Also checked against systemd 259 offline (systemd-analyze verify on a temp unit file). The old output gave Failed to resolve unit specifiers in /tmp/100%Qbackup/deck.log, ignoring and a fatal ExecStart error. The new output verifies clean.
  • Changed existing guards (none removed): app-host.test.ts's three PATH cases now call shellEnv(...).PATH. The skip-gate register counts 27 win32 sites and 56 Windows skips.
  • No browser check: the only page change is one new entry in the banner's reason map, and a test asserts it is there.

…and and the login item

process.execPath has its links resolved, so a Homebrew or linked tarball node wrote a versioned path into settings.json and the login job, and the next upgrade left every hook exiting 127. Both now use the PATH entry that links to the running node, or Homebrew's opt/ link, and fall back to execPath.
The job carried only the scope directories, so a deck started at login ran without AGENTS_DECK_NO_REPORTS, AGENTS_DECK_NO_LAN and the rest of the documented settings the installing shell had set. Every variable in README's environment table now rides along by name, and nothing else from the shell does.
…shell sets

An app opened from the Dock or a launcher read only PATH from the login shell, so its deck ran without AGENTS_DECK_NO_REPORTS, AGENTS_DECK_NO_LAN, CLAUDE_CONFIG_DIR and the rest of README's environment table. The shell is now asked for those names alone, and the app takes them onto its own environment before it looks for a deck.
Only npx's cache was recognised, so the other one-off runners got a login item naming a file in their cache and an in-app npm i -g the away-update repeated every thirty minutes without moving the running copy. They are now refused both, and the banner shows the global install as a command instead.
…romising one

With no systemd, systemctl was missing or refused and that read as a unit file systemd would pick up at the next login, so the first start said ccdeck would start at login and never offered again. installService now asks whether systemd is the init before writing anything, and reports the refusal.
…t only Environment=

systemd resolves specifiers in StandardOutput=, StandardError= and ExecStart too, and variables in ExecStart, so a deck home or install path with a % in it named somewhere else, or failed the unit outright. Every value is now escaped, and an ExecStart argument with a quote or backslash is quoted with systemd's escapes.
@BarganConstantin
BarganConstantin merged commit b57070a into development Oct 5, 2026
10 checks passed
@BarganConstantin BarganConstantin mentioned this pull request Oct 5, 2026
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