* fix(#3017): codex SessionStart hook uses absolute node, not bare 'node'
PR #3002fixed#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.