Files
get-shit-done/.changeset/codex-bare-node-fix.md
Tom Boucher 8e25eb6546 fix(#3017): codex SessionStart hook uses absolute node, not bare 'node' (#3022)
* fix(#3017): codex SessionStart hook uses absolute node, not bare 'node'

PR #3002 fixed #2979 for settings.json-based managed JS hooks (Claude
Code, Gemini, Antigravity) by routing through buildHookCommand() →
resolveNodeRunner(), emitting the absolute Node binary path so hooks
resolve under GUI/minimal-PATH runtimes (/usr/bin:/bin:/usr/sbin:/sbin)
where nvm/Homebrew/Volta-installed node is not on PATH.

The Codex install path bypassed both helpers — line 7935 of bin/install.js
wrote `command = "node ${path}"` directly into config.toml. So Codex
SessionStart hook still failed with exit 127 ("node: command not found")
under the same minimal-PATH conditions PR #3002 was meant to close.

Fix:
- Add buildCodexHookBlock(targetDir, { absoluteRunner, eol }) — a pure
  helper that emits the toml hook block with the absolute runner. Returns
  null when absoluteRunner is null so the caller skips registration with
  a warning instead of writing a broken bare-node hook.
- Add rewriteLegacyCodexHookBlock(content, absoluteRunner) — mirror of
  rewriteLegacyManagedNodeHookCommands for the toml surface, so
  reinstall migrates a 1.39.x bare-node config.toml to the absolute form.
  Uses basename equality (CODEX_MANAGED_HOOK_BASENAMES set) so user-
  authored bare-node hooks are left alone.
- Replace the inline string-concat at line 7935 with a call to the new
  helper, threaded with the detected line ending so CRLF files stay CRLF.
- On the codex reinstall path, call rewriteLegacyCodexHookBlock first so
  existing bare-node entries get migrated before the new entry is added.

Tests:
- bug-3017-codex-hook-absolute-node.test.cjs (9 tests, all typed-IR):
  - buildCodexHookBlock emits absolute runner, parses to expected fields
  - returns null on missing runner (caller skips)
  - integrates with resolveNodeRunner() in the live process
  - rewriteLegacyCodexHookBlock migrates managed bare-node entries
  - leaves user-authored bare-node hooks alone (basename allowlist)
  - leaves entries with absolute runner unchanged (idempotent)
  - returns content unchanged when absoluteRunner is null
- codex-config.test.cjs e2e expectation updated to match new shape:
  parsed.hooks.SessionStart[0].hooks[0].command now equals
  '"<process.execPath>" "<hookPath>"' instead of 'node <hookPath>'.

Verification:
- 9/9 pass on the new regression test
- 179/179 pass across all codex-touching test files
- 6767/6767 pass on full suite, lint-no-source-grep clean
- Adheres to typed-IR / CONTRIBUTING.md "Prohibited: Raw Text Matching":
  parseCodexHookBlock returns a typed record; assertions are on
  structured fields (runner, hookPath, type, hasMarker), not stdout regex.

Closes #3017

* test(#3017): tighten runner assertions to exact process.execPath (CR)

CodeRabbit on PR #3022 (3 findings, 2 actionable + 1 nitpick):

1. .changeset/codex-bare-node-fix.md:3 — replace `pr: TBD` with
   `pr: 3022` so changeset metadata is traceable.

2. tests/bug-3017-codex-hook-absolute-node.test.cjs:81-146 — the test
   asserted `parsed.runner !== 'node'` and `parsed.runner.includes('/node')`,
   which would false-positive on any absolute path containing '/node'
   (e.g. /Users/x/notnode/foo). Tightened to compare against the EXACT
   absolute path supplied by the caller (after stripping toml + JSON
   escape layers via a new unescapeRunner() helper). The live-process
   integration test now compares against process.execPath exactly. The
   rewriteLegacyCodexHookBlock test also uses exact-equality.

3. Nitpick (skipped): use repository's TOML parser for parsing instead
   of bespoke regex. The hand-rolled parser is small, scoped, and
   fully tested by these structural assertions; pulling in a TOML lib
   for tests would create a circular dependency on the SUT (the
   installer's own parser). Leaving as-is.

Verification: 9/9 pass on regression test, 6767/6767 full suite, lint clean.
2026-05-02 11:45:30 -04:00

509 B

type, pr
type pr
Fixed 3022

Codex SessionStart hook now uses absolute Node binary path — closes the gap left after #3002. The Codex install path wrote command = "node ${path}" directly into config.toml, bypassing resolveNodeRunner(). Under GUI/minimal-PATH runtimes (/usr/bin:/bin:/usr/sbin:/sbin), bare node failed to resolve, exit 127. Now routed through new buildCodexHookBlock() helper. Reinstall path migrates legacy bare-node entries via new rewriteLegacyCodexHookBlock(). See #3017.