Skip to content

Fix Codex hook installation to use hooks.json instead of inline TOML hooks#2640

Closed
hanzckernel wants to merge 10 commits intogsd-build:mainfrom
hanzckernel:fix/codex-hooks-json-migration
Closed

Fix Codex hook installation to use hooks.json instead of inline TOML hooks#2640
hanzckernel wants to merge 10 commits intogsd-build:mainfrom
hanzckernel:fix/codex-hooks-json-migration

Conversation

@hanzckernel
Copy link
Copy Markdown

@hanzckernel hanzckernel commented Apr 23, 2026

Fix PR

Using the wrong template?
— Enhancement: use enhancement template
— Feature: use feature template


Linked Issue

Fixes #2637

The linked issue is still awaiting maintainer confirmation (confirmed-bug label not present yet).


What was broken

GSD installed a Codex SessionStart update hook as inline [[hooks]] TOML in config.toml. Newer Codex versions expect hooks in hooks.json, so model-setting updates could fail with invalid type: map, expected a sequence. The install/uninstall path also had edge cases where managed hook references could survive in malformed or legacy configs.

What this fix does

Moves the managed Codex update hook to hooks.json, preserves user-owned hook content, avoids rewriting malformed existing hooks.json files, tracks Codex hook files in the manifest, and removes only exact managed GSD hook references during uninstall across both hooks.json and legacy inline TOML hook blocks.

Root cause

The installer treated Codex hook configuration like inline TOML hook arrays instead of Codex’s hooks.json format, and the cleanup logic matched only narrower legacy shapes than the real world configs users could carry forward.

Testing

How I verified the fix

  • Added/updated Codex regression coverage for:
    • hooks.json migration and merge behavior
    • malformed hooks.json preservation without rewrite
    • uninstall cleanup of exact managed hook references from malformed/non-SessionStart/inline TOML cases
    • compact TOML formatting and header-comment variants
    • root inline-table features = { codex_hooks = true } configs migrate to hooks.json without invalid dotted-key rewrites
    • Codex hook-file manifest tracking
  • Ran:
    • npm run build:hooks && node --test tests/codex-config.test.cjs
    • git diff --check
    • npm test

Regression test added?

  • Yes — added tests that would have caught the bug and the uninstall/migration edge cases
  • No — explain why:

Platforms tested

  • macOS
  • Windows (including backslash path handling)
  • Linux
  • N/A (not platform-specific)

Runtimes tested

  • Claude Code
  • Gemini CLI
  • OpenCode
  • Other: Codex
  • N/A (not runtime-specific)

Checklist

  • Issue linked above with Fixes #NNN
  • Linked issue has the confirmed-bug label
  • Fix is scoped to the reported bug — no unrelated changes included
  • Regression test added (or explained why not)
  • All existing tests pass (npm test)
  • CHANGELOG.md updated if this is a user-facing fix
  • No unnecessary dependencies added

Breaking changes

None

Summary by CodeRabbit

  • Changed
    • Installer now records managed SessionStart hooks in a dedicated managed hooks file, merging into existing entries without overwriting user content, preserving malformed files when unsafe to migrate, preferring bundled distributable assets with fallback, improving legacy inline hook cleanup and matching, avoids deleting hook assets when cleanup fails, and refreshes the file manifest after install.
  • Tests
    • Added end-to-end coverage for install/uninstall migration, user-hook/root-key preservation, malformed-file fallbacks, legacy escaping variants, and manifest tracking of installed hook assets.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Installer/uninstaller for Codex hooks now manage a GSD-owned SessionStart handler via hooks.json (merge/remove), migrate or remove legacy inline TOML hooks safely, prefer hooks/dist/ when copying assets, preserve malformed/user hooks.json when unsafe, and update hook manifest tracking; tests expanded accordingly.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Documented Codex hook handling changes, follow-up notes about manifest refresh and legacy TOML cleanup.
Installer logic
bin/install.js
Reworked hook management: shift from injecting inline TOML to merging/removing managed SessionStart entries in hooks.json; added safe-preservation flags, fallback to hooks/ when hooks/dist/ missing, broadened manifest tracking, and updated exported helper signature stripGsdFromCodexConfig(content, configDir).
Tests (codex config e2e)
tests/codex-config.test.cjs
Added helpers for controlled install/uninstall, hooks.json IO helpers, many install/uninstall cases validating merge/idempotence, malformed/zero-byte preservation, and precise managed-command matching.
Tests (manifest)
tests/install-hooks-copy.test.cjs
Added assertion that gsd-file-manifest.json includes hooks/gsd-check-update.js for Codex installs.
Metadata/manifest
package.json
Minor manifest metadata tweak (see diff).

Sequence Diagram(s)

sequenceDiagram
  participant Installer as Installer (bin/install.js)
  participant FS as Filesystem
  participant ConfigToml as config.toml
  participant HooksJson as hooks.json (JSONC)
  participant HooksDir as hooks/dist/ or hooks/
  participant Manifest as gsd-file-manifest.json

  Installer->>FS: resolve configDir
  Installer->>ConfigToml: read config.toml
  Installer->>HooksJson: read hooks.json (if present)
  alt hooks.json parseable and safe
    Installer->>HooksJson: remove prior managed entries
    Installer->>HooksJson: merge managed SessionStart handler
    HooksJson-->>FS: write updated hooks.json
  else hooks.json malformed or unsafe
    Installer->>ConfigToml: remove inline TOML only when clearly GSD-managed
    Installer-->>FS: leave hooks.json bytes untouched and skip enabling codex_hooks
  end
  Installer->>HooksDir: copy hook asset (prefer hooks/dist/ then hooks/)
  Installer->>Manifest: write/append hooks entry to gsd-file-manifest.json
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

size/XL

Suggested reviewers

  • glittercowboy

Poem

"I hopped through configs, tidy and spry,
Merged SessionStart hooks without a cry.
I left odd JSON bytes safe in their lair,
Copied scripts from dist with gentle care.
A rabbit's patch — small fixes, done with flair." 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the main change: moving Codex hook installation from inline TOML to hooks.json format, which is the core objective of this PR.
Linked Issues check ✅ Passed The PR implements core mitigation for issue #2637 by migrating Codex hooks to hooks.json format (avoiding inline TOML incompatibility), preserving malformed configs, and improving uninstall cleanup to prevent hook reference survival.
Out of Scope Changes check ✅ Passed All changes are scoped to Codex hook management: install/uninstall logic, hook manifest tracking, hooks.json handling, and corresponding test coverage—no unrelated modifications.
Description check ✅ Passed The PR uses the correct Fix template and includes all required sections: linked issue, root cause explanation, testing approach with regression tests, platform/runtime coverage, and checklist completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
CHANGELOG.md (1)

24-24: Changelog entry is accurate but incomplete—missing key user-facing guarantees.

The entry correctly describes the migration and the fix, but omits several important behaviors users need to know about:

  1. User hook preservation: The PR preserves user hooks that aren't the exact managed command and preserves malformed hooks.json content instead of rewriting it—these safety guarantees aren't mentioned.
  2. Selective file deletion: On uninstall, hooks.json is deleted only when it contains GSD-only content; otherwise user root keys are preserved—not mentioned.
  3. "Managed" definition: What constitutes a "managed hooks.json entry" (exact command match) vs user-owned hooks is unclear.

Users upgrading need reassurance their customizations won't be lost and their files won't be corrupted.

📝 Suggested expansion for completeness
-- **Codex install now writes the GSD SessionStart hook into `hooks.json` instead of inline `[[hooks]]` TOML** — fixes invalid `config.toml` shapes that made Codex reject model-setting updates with `invalid type: map, expected a sequence`; uninstall now removes only the managed `hooks.json` entry and source-checkout installs fall back to `hooks/` when `hooks/dist/` is absent.
+- **Codex install now writes the GSD SessionStart hook into `hooks.json` instead of inline `[[hooks]]` TOML** — fixes invalid `config.toml` shapes that made Codex reject model-setting updates with `invalid type: map, expected a sequence`. Install merges the managed GSD SessionStart hook with existing hooks.json content without clobbering user-owned hooks, and preserves malformed existing hooks.json files instead of rewriting them. Uninstall removes only the exact managed GSD hook references from hooks.json (and legacy inline TOML blocks), preserves user root keys, and deletes the file only when it is GSD-only. Source-checkout installs fall back to `hooks/` when `hooks/dist/` is absent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` at line 24, Update the changelog entry for the GSD SessionStart
hook migration to explicitly state three user-facing guarantees: (1) user hooks
are preserved unless they exactly match the managed GSD command (clarify the
"managed" definition as an exact command string match to the GSD SessionStart
hook), (2) malformed hooks.json content is preserved (we do not rewrite or
normalize existing user files), and (3) uninstall deletes hooks.json only when
it contains GSD-only managed entries and will preserve root keys or mixed
user-owned entries; mention fallback behavior for source-checkout installs to
hooks/ when hooks/dist/ is absent.
bin/install.js (1)

6464-6497: Track the new Codex hook files in the manifest too.

This branch now installs managed files under hooks/, but writeManifest() still skips Codex hooks. A local edit to hooks/gsd-check-update.js will be wiped on reinstall without going through the existing patch-backup flow.

♻️ Follow-up outside this hunk
-  // Track hook files so saveLocalPatches() can detect user modifications
-  // Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
-  if (!isCodex && !isCopilot && !isCline) {
+  // Track hook files so saveLocalPatches() can detect user modifications.
+  // Codex now also installs managed hooks under hooks/.
+  if (!isCopilot && !isCline) {
     const hooksDir = path.join(configDir, 'hooks');
     if (fs.existsSync(hooksDir)) {
       for (const file of fs.readdirSync(hooksDir)) {
         if (file.startsWith('gsd-') && (file.endsWith('.js') || file.endsWith('.sh'))) {
           manifest.files['hooks/' + file] = fileHash(path.join(hooksDir, file));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/install.js` around lines 6464 - 6497, writeManifest() currently skips the
newly-installed Codex hook files so edits under hooks/ are not tracked; update
the install flow to add those files to the manifest (or modify writeManifest to
include the hooks/ directory) by recording each created hook path
(codexHooksDest entries) with the same managed-file metadata used for other
installed files, ensuring hooks/gsd-check-update.js and any .sh/.js files are
added to the manifest so the patch-backup/restore logic applies on reinstall;
reference the variables/functions codexHooksSrc, codexHooksDest, and
writeManifest() and ensure the code path that writes files also invokes the
manifest-add routine (or that writeManifest() enumerates targetDir/hooks) so
hooks are tracked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/install.js`:
- Around line 2094-2099: The current loop filters lines by exact-line regex
/^\s*\[\[hooks\]\]\s*$/ which fails for valid TOML like '[[hooks]] # comment';
update the check inside the for-loop that examines line.text (the code around
variables lines and kept) to use a TOML header/table-array parser instead of the
strict regex: parse the line as a TOML header (or use an existing TOML header
utility) and treat any header that represents the table-array named "hooks"
(i.e., [[hooks]] even with trailing comments/whitespace) as the match to
skip/handle, so managed inline hook blocks are reliably detected during
install/uninstall cleanup.

In `@tests/codex-config.test.cjs`:
- Around line 1025-1053: The test currently writes malformed hooks with
writeCodexHooksJson() and only compares parsed JSON, which won't detect a
rewrite that preserves semantic content; instead, write a hand-authored
non-canonical JSON string for the hooks.json fixture (e.g., with extra spacing,
ordering, or comments-like whitespace) and write it directly to disk before
calling runCodexInstall(), then assert the raw file contents (via readFileSync
or an existing raw-reader helper) are byte-for-byte identical after install;
keep the existing parsed-JSON and config assertions (readCodexHooksJson,
readCodexConfig, countMatches) but add this raw-content equality check to ensure
install() does not reserialize/overwrite malformed hooks.json.

---

Nitpick comments:
In `@bin/install.js`:
- Around line 6464-6497: writeManifest() currently skips the newly-installed
Codex hook files so edits under hooks/ are not tracked; update the install flow
to add those files to the manifest (or modify writeManifest to include the
hooks/ directory) by recording each created hook path (codexHooksDest entries)
with the same managed-file metadata used for other installed files, ensuring
hooks/gsd-check-update.js and any .sh/.js files are added to the manifest so the
patch-backup/restore logic applies on reinstall; reference the
variables/functions codexHooksSrc, codexHooksDest, and writeManifest() and
ensure the code path that writes files also invokes the manifest-add routine (or
that writeManifest() enumerates targetDir/hooks) so hooks are tracked.

In `@CHANGELOG.md`:
- Line 24: Update the changelog entry for the GSD SessionStart hook migration to
explicitly state three user-facing guarantees: (1) user hooks are preserved
unless they exactly match the managed GSD command (clarify the "managed"
definition as an exact command string match to the GSD SessionStart hook), (2)
malformed hooks.json content is preserved (we do not rewrite or normalize
existing user files), and (3) uninstall deletes hooks.json only when it contains
GSD-only managed entries and will preserve root keys or mixed user-owned
entries; mention fallback behavior for source-checkout installs to hooks/ when
hooks/dist/ is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa9ae4b5-8276-47e3-b5f6-b1efa5ca6b0e

📥 Commits

Reviewing files that changed from the base of the PR and between a72bebb and 7f61c30.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • bin/install.js
  • tests/codex-config.test.cjs

Comment thread bin/install.js Outdated
Comment thread bin/install.js
Comment thread tests/codex-config.test.cjs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bin/install.js (1)

6480-6536: ⚠️ Potential issue | 🟠 Major

Refresh the manifest after Codex hook installation.

The new hook-file tracking never sees the files copied here, because writeManifest() still runs on Line 6419 before this Codex-only block. saveLocalPatches() will therefore miss user edits to Codex hook files on the next update.

🔧 Proposed fix
     } catch (e) {
       console.warn(`  ${yellow}⚠${reset}  Could not configure Codex hooks: ${e.message}`);
     }
+
+    writeManifest(targetDir, runtime);
     return { settingsPath: null, settings: null, statuslineCommand: null, runtime, configDir: targetDir };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/install.js` around lines 6480 - 6536, The Codex hooks files are copied
and hooks.json/config updated after writeManifest() ran earlier, so
saveLocalPatches() misses those files; call writeManifest() again right after
the Codex hooks block (after the console.log that prints "Configured Codex hooks
(SessionStart via hooks.json)") so the manifest reflects the newly written hook
files and saveLocalPatches() will pick them up; place the call adjacent to the
existing code that uses buildHookCommand, mergeGsdIntoCodexHooksJson and
stripManagedGsdCodexInlineHooks, and wrap it in a try/catch (or await if
writeManifest is async) so any manifest refresh errors are logged but don’t
break the install.
♻️ Duplicate comments (1)
bin/install.js (1)

2095-2096: ⚠️ Potential issue | 🟡 Minor

Decode TOML literal strings too.

JSON.parse() fixes basic strings, but TOML literal strings still leave doubled apostrophes intact. A command like command = 'node "/Users/O''Brien/.codex/hooks/gsd-check-update.js"' will miss managedCommands, so the legacy [[hooks]] block survives migration/uninstall.

🔧 Proposed fix
-  const literalMatch = valueText.match(/^'(.*)'$/);
-  return literalMatch ? literalMatch[1] : null;
+  const literalMatch = valueText.match(/^'(.*)'$/);
+  return literalMatch ? literalMatch[1].replace(/''/g, "'") : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/install.js` around lines 2095 - 2096, The current literal string handling
(using literalMatch on valueText) returns the inner content but doesn't
un-escape TOML literal string doubled apostrophes, so values like node
"/Users/O''Brien/..." remain incorrect; update the code path that handles
literalMatch (the valueText.match(/^'(.*)'$/) branch) to return the captured
content with TOML literal apostrophes decoded (replace doubled single quotes
with a single quote) so managedCommands and hook paths are parsed correctly.
🧹 Nitpick comments (1)
tests/codex-config.test.cjs (1)

93-97: Minor readability improvement for the Array.isArray check.

The expression Array.isArray(group && group.hooks) works correctly but reads awkwardly. When group is falsy, it evaluates Array.isArray(false) which returns false—correct but confusing.

🔧 Suggested clarification
   return Object.values(readCodexHooksJson(codexHome).hooks || {})
     .flatMap(eventGroups => Array.isArray(eventGroups) ? eventGroups : [])
-    .flatMap(group => Array.isArray(group && group.hooks) ? group.hooks : [])
+    .flatMap(group => (group && Array.isArray(group.hooks)) ? group.hooks : [])
     .filter(hook => isManagedGsdUpdateHookCommand(hook && hook.command, codexHome))
     .length;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/codex-config.test.cjs` around lines 93 - 97, The Array.isArray(group &&
group.hooks) check is confusing when group is falsy; change it to explicitly
check the property (e.g., use Array.isArray(group?.hooks) or group &&
Array.isArray(group.hooks)) so intent is clear; update the chain in the return
expression that uses readCodexHooksJson(...).hooks, the flatMap that references
group and group.hooks, and keep the rest of the pipeline (including
isManagedGsdUpdateHookCommand) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/install.js`:
- Around line 5081-5098: The catch for cleaning hooks.json currently treats
failures as if the JSON were safely removed later; instead, when
stripGsdFromCodexHooksJson or fs.readFileSync throws, do NOT mark the repo as
having had GSD hooks removed nor allow the later removal of the managed hook
file (hooks/gsd-check-update.js). Concretely: in the catch block for the
hooksJsonPath handling, set a local flag like hooksJsonCleanFailed = true (or
rethrow) and do not increment removedCount; then update the later removal logic
that targets hooks/gsd-check-update.js to skip deletion if hooksJsonCleanFailed
is true (or abort when an error was thrown), so a malformed hooks.json isn't
left pointing to a deleted command.

---

Outside diff comments:
In `@bin/install.js`:
- Around line 6480-6536: The Codex hooks files are copied and hooks.json/config
updated after writeManifest() ran earlier, so saveLocalPatches() misses those
files; call writeManifest() again right after the Codex hooks block (after the
console.log that prints "Configured Codex hooks (SessionStart via hooks.json)")
so the manifest reflects the newly written hook files and saveLocalPatches()
will pick them up; place the call adjacent to the existing code that uses
buildHookCommand, mergeGsdIntoCodexHooksJson and
stripManagedGsdCodexInlineHooks, and wrap it in a try/catch (or await if
writeManifest is async) so any manifest refresh errors are logged but don’t
break the install.

---

Duplicate comments:
In `@bin/install.js`:
- Around line 2095-2096: The current literal string handling (using literalMatch
on valueText) returns the inner content but doesn't un-escape TOML literal
string doubled apostrophes, so values like node "/Users/O''Brien/..." remain
incorrect; update the code path that handles literalMatch (the
valueText.match(/^'(.*)'$/) branch) to return the captured content with TOML
literal apostrophes decoded (replace doubled single quotes with a single quote)
so managedCommands and hook paths are parsed correctly.

---

Nitpick comments:
In `@tests/codex-config.test.cjs`:
- Around line 93-97: The Array.isArray(group && group.hooks) check is confusing
when group is falsy; change it to explicitly check the property (e.g., use
Array.isArray(group?.hooks) or group && Array.isArray(group.hooks)) so intent is
clear; update the chain in the return expression that uses
readCodexHooksJson(...).hooks, the flatMap that references group and
group.hooks, and keep the rest of the pipeline (including
isManagedGsdUpdateHookCommand) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b4101ad-bd3b-47e3-90e5-e29d0db89f1a

📥 Commits

Reviewing files that changed from the base of the PR and between e4998db and 11621c3.

📒 Files selected for processing (2)
  • bin/install.js
  • tests/codex-config.test.cjs

Comment thread bin/install.js
Refresh the Codex hook manifest after Codex-specific hook installation, decode doubled apostrophes when matching legacy TOML literal commands, and preserve the update hook file when hooks.json cannot be cleaned during uninstall.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
bin/install.js (1)

5308-5318: ⚠️ Potential issue | 🟡 Minor

gsd-check-update-worker.js is still left behind on uninstall.

Codex install now copies and manifests the full hook set, but this deletion list still omits hooks/gsd-check-update-worker.js, so uninstall leaves a managed GSD artifact behind.

🔧 Proposed fix
-    const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-context-monitor.js', 'gsd-prompt-guard.js', 'gsd-read-guard.js', 'gsd-read-injection-scanner.js', 'gsd-workflow-guard.js', 'gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh'];
+    const gsdHooks = ['gsd-statusline.js', 'gsd-check-update.js', 'gsd-check-update-worker.js', 'gsd-context-monitor.js', 'gsd-prompt-guard.js', 'gsd-read-guard.js', 'gsd-read-injection-scanner.js', 'gsd-workflow-guard.js', 'gsd-session-state.sh', 'gsd-validate-commit.sh', 'gsd-phase-boundary.sh'];
@@
-      if (isCodex && codexHooksJsonCleanFailed && hook === 'gsd-check-update.js') {
+      if (isCodex && codexHooksJsonCleanFailed && (hook === 'gsd-check-update.js' || hook === 'gsd-check-update-worker.js')) {
         continue;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/install.js` around lines 5308 - 5318, The uninstall hook list (gsdHooks
array in bin/install.js) omits the managed worker file
gsd-check-update-worker.js so it isn't removed; add 'gsd-check-update-worker.js'
to the gsdHooks array (or otherwise ensure the same deletion logic that uses
hooksDir, isCodex, codexHooksJsonCleanFailed, and the hook loop applies to that
filename) so the file is checked for existence and unlinked during the uninstall
pass.
🧹 Nitpick comments (1)
tests/codex-config.test.cjs (1)

75-97: Align managed-hook command matching with installer variants.

At Line [75], the helper only tracks quoted absolute command forms. The installer’s managed-command matcher supports quoted/unquoted and $HOME-portable forms too, so countGsdUpdateHooksInHooksJson() (Line [87]) can miss valid managed entries.

♻️ Suggested parity update
 function getManagedGsdUpdateHookCommands(codexHome) {
-  const hooksDir = path.join(codexHome, 'hooks').replace(/\\/g, '/');
-  return new Set([
-    `node "${hooksDir}/gsd-check-update.js"`,
-    `node "${hooksDir}/gsd-update-check.js"`,
-  ]);
+  const normalized = codexHome.replace(/\\/g, '/');
+  const home = os.homedir().replace(/\\/g, '/');
+  const portableBase = normalized.startsWith(home)
+    ? '$HOME' + normalized.slice(home.length)
+    : normalized;
+
+  const commands = new Set();
+  for (const hookName of ['gsd-check-update.js', 'gsd-update-check.js']) {
+    for (const baseDir of new Set([normalized, portableBase])) {
+      const hookPath = `${baseDir}/hooks/${hookName}`;
+      commands.add(`node ${hookPath}`);
+      commands.add(`node "${hookPath}"`);
+    }
+  }
+  return commands;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/codex-config.test.cjs` around lines 75 - 97, The current helper only
recognizes quoted absolute command strings in getManagedGsdUpdateHookCommands,
causing isManagedGsdUpdateHookCommand/countGsdUpdateHooksInHooksJson to miss
installer-supported variants; update getManagedGsdUpdateHookCommands and/or
isManagedGsdUpdateHookCommand to accept both quoted and unquoted forms and
$HOME-portable forms by generating or matching variants for node
"<hooksDir>/gsd-*.js", node <hooksDir>/gsd-*.js, node "$HOME/..../gsd-*.js" and
node $HOME/..../gsd-*.js (or use a regex that normalizes optional quotes and
replaces codexHome with \$HOME) so countGsdUpdateHooksInHooksJson correctly
detects managed hooks across installer variants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/install.js`:
- Around line 2066-2071: The matcher for legacy hook commands currently only
adds bare (`node ${hookPath}`) and double-quoted (`node "${hookPath}"`) forms;
add the single-quoted form as well so decoded TOML literal strings like `node
'$HOME/.../gsd-check-update.js'` are recognized. In the loop that iterates
hookName and baseDir (referencing hookName, baseDir, normalized, portableBase)
add a third commands.add entry that constructs the single-quoted command (e.g.,
commands.add(`node '${hookPath}'`)) alongside the existing two variants so the
single-quoted legacy form is cleaned up too.
- Around line 2306-2310: The code treats a zero-byte existingContent as falsy
and behaves as if the file is missing; update mergeGsdIntoCodexHooksJson to
distinguish "no file" (null/undefined) from an empty string by using explicit
null/undefined checks (e.g., existingContent != null) when computing eol
(detectLineEnding) and parsed (parseCodexHooksJson) so an empty hooks.json is
preserved rather than being replaced; reference mergeGsdIntoCodexHooksJson, eol,
parsed, detectLineEnding, and parseCodexHooksJson when making the change.

---

Outside diff comments:
In `@bin/install.js`:
- Around line 5308-5318: The uninstall hook list (gsdHooks array in
bin/install.js) omits the managed worker file gsd-check-update-worker.js so it
isn't removed; add 'gsd-check-update-worker.js' to the gsdHooks array (or
otherwise ensure the same deletion logic that uses hooksDir, isCodex,
codexHooksJsonCleanFailed, and the hook loop applies to that filename) so the
file is checked for existence and unlinked during the uninstall pass.

---

Nitpick comments:
In `@tests/codex-config.test.cjs`:
- Around line 75-97: The current helper only recognizes quoted absolute command
strings in getManagedGsdUpdateHookCommands, causing
isManagedGsdUpdateHookCommand/countGsdUpdateHooksInHooksJson to miss
installer-supported variants; update getManagedGsdUpdateHookCommands and/or
isManagedGsdUpdateHookCommand to accept both quoted and unquoted forms and
$HOME-portable forms by generating or matching variants for node
"<hooksDir>/gsd-*.js", node <hooksDir>/gsd-*.js, node "$HOME/..../gsd-*.js" and
node $HOME/..../gsd-*.js (or use a regex that normalizes optional quotes and
replaces codexHome with \$HOME) so countGsdUpdateHooksInHooksJson correctly
detects managed hooks across installer variants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fc5ded1a-9cbc-461c-adcf-21bed2689081

📥 Commits

Reviewing files that changed from the base of the PR and between 11621c3 and 4167790.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • bin/install.js
  • tests/codex-config.test.cjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Comment thread bin/install.js
Comment thread bin/install.js
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/codex-config.test.cjs (1)

75-97: Align test managed-command matching with production variants

On Line 75, getManagedGsdUpdateHookCommands only matches double-quoted absolute commands. Production cleanup logic matches additional forms (unquoted, single-quoted, and $HOME-portable), so this helper can undercount managed hooks and miss regression cases.

Proposed fix
 function getManagedGsdUpdateHookCommands(codexHome) {
-  const hooksDir = path.join(codexHome, 'hooks').replace(/\\/g, '/');
-  return new Set([
-    `node "${hooksDir}/gsd-check-update.js"`,
-    `node "${hooksDir}/gsd-update-check.js"`,
-  ]);
+  const home = os.homedir().replace(/\\/g, '/');
+  const normalized = codexHome.replace(/\\/g, '/');
+  const portableBase = normalized.startsWith(home)
+    ? '$HOME' + normalized.slice(home.length)
+    : normalized;
+
+  const commands = new Set();
+  for (const hookName of ['gsd-check-update.js', 'gsd-update-check.js']) {
+    for (const baseDir of new Set([normalized, portableBase])) {
+      const hookPath = `${baseDir}/hooks/${hookName}`;
+      commands.add(`node ${hookPath}`);
+      commands.add(`node "${hookPath}"`);
+      commands.add(`node '${hookPath}'`);
+    }
+  }
+  return commands;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/codex-config.test.cjs` around lines 75 - 97,
getManagedGsdUpdateHookCommands currently only returns double-quoted absolute
command strings so tests miss other forms the production cleanup handles; update
getManagedGsdUpdateHookCommands to generate and return all equivalent variants
for each script (double-quoted, single-quoted, unquoted) and versions that use
$HOME instead of the absolute codexHome path, then keep using
isManagedGsdUpdateHookCommand and countGsdUpdateHooksInHooksJson as-is so the
filter will correctly recognize managed hooks in their alternative forms.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/install.js`:
- Around line 6527-6543: The current success log runs even when
ensureCodexHooksFeature() returned an inline-table ownership (e.g., features = {
codex_hooks = true }) which prevents migration because
hasEnabledCodexHooksFeature() stays false; detect that case by checking
codexHooksFeature.ownership (or the equivalent ownership flag returned by
ensureCodexHooksFeature) and treat it as a non-migrated inline-table case:
either perform the conversion path (so
stripManagedGsdCodexInlineHooks/mergeGsdIntoCodexHooksJson run) or, if
conversion isn’t done automatically, emit a warning and skip printing the
success message. Concretely, update the conditional after computing
codexHooksFeature and nextConfigContent to branch when
codexHooksFeature.ownership === 'inline-table' (or the actual ownership token)
and handle that case (run migration or log a warning and avoid the “Configured
Codex hooks” console.log) so we don’t claim success when migration cannot run.

---

Nitpick comments:
In `@tests/codex-config.test.cjs`:
- Around line 75-97: getManagedGsdUpdateHookCommands currently only returns
double-quoted absolute command strings so tests miss other forms the production
cleanup handles; update getManagedGsdUpdateHookCommands to generate and return
all equivalent variants for each script (double-quoted, single-quoted, unquoted)
and versions that use $HOME instead of the absolute codexHome path, then keep
using isManagedGsdUpdateHookCommand and countGsdUpdateHooksInHooksJson as-is so
the filter will correctly recognize managed hooks in their alternative forms.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e120fdfc-2b3a-40c9-8fd6-e2abfe52e74a

📥 Commits

Reviewing files that changed from the base of the PR and between 4167790 and 1b6b6e2.

📒 Files selected for processing (2)
  • bin/install.js
  • tests/codex-config.test.cjs

Comment thread bin/install.js Outdated
Copy link
Copy Markdown
Collaborator

@trek-e trek-e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — request changes

Verdict

Request changes. The migration is well-tested and the edge-case handling (malformed hooks.json, zero-byte file, TOML literal apostrophes, managed-command cleanup across event names) is careful work. Two concrete problems before merge.

RCA — one gap

Missing version guard. Issue #2637 explicitly asked for "detect affected Codex versions and skip enabling hooks" or equivalent mitigation. This PR unconditionally switches every Codex install to hooks.json (bin/install.js:6606-6636). Older Codex releases that only understood the inline [[hooks]] TOML are now silently unhooked — the SessionStart update check stops firing because the hooks.json format didn't exist yet. No runtime/version detection, no fallback. Gall's law: the new format works for 0.124.0, but you've replaced a working-for-old-Codex system with one that assumes everyone is on the hooks.json-aware build. Goodhart: the test "does not write legacy inline TOML hooks" (tests/codex-config.test.cjs:769-780) optimizes for a metric (no [[hooks]]) that is exactly what older Codex needs. Either probe codex --version or detect hooks.json support capability — the README-level promise of "update checks via SessionStart" is now false on <0.124.0 without telling the user.

Rubber-duck — followups that look like smells

  • getManagedCodexSessionStartCommands (bin/install.js:2419-2421) is a one-line alias of getManagedCodexLegacyInlineCommands. Kernighan: the alias obscures that session-start-json and legacy-inline-toml commands are the same string set — delete the alias or give it a reason to exist. Peter's principle applied to function names: they rise to a level of abstraction past their usefulness.
  • bin/install.js:5975-5977 quietly widens writeManifest hook tracking from !isCodex && !isCopilot && !isCline to !isCopilot && !isCline. Correct direction, but out of scope for a PR titled "hooks.json migration" — mention it in the PR body or split. Knuth: premature generalization of the gate is how test coverage for non-Codex runtimes silently drifts.

Blame

Inline [[hooks]] TOML was introduced in 70f1f8b "feat: add Codex hooks support for SessionStart (#1020)" — Tom Boucher, 2026-03-17, +30 lines to bin/install.js. That commit hardcoded the now-deprecated format.

Required changes

  1. Add Codex version detection (or capability probe for hooks.json support) and fall back to legacy [[hooks]] write on older Codex. Skip hook install entirely on ambiguous detection rather than silently breaking update checks.
  2. Inline or rename getManagedCodexSessionStartCommands so the call site at bin/install.js:2438/2506 reads honestly.
  3. Note the writeManifest gate change in the PR body (or split).

CI is green. Diff is +1106/−200 across 4 files, tests are the bulk of it, and the test matrix is good. Fix the version guard and this ships.

@trek-e trek-e added bug Something isn't working area: installer Installation, CLI setup size/L review: changes requested PR reviewed — changes required before merge needs changes Review requested changes that must be addressed labels Apr 24, 2026
Detect the installed Codex CLI before configuring update hooks. Keep legacy inline SessionStart hooks for Codex versions before 0.124.0, use hooks.json for 0.124.0 and newer, and skip hook configuration when support cannot be determined safely. Add regression coverage for version selection, legacy fallback, and skip mode.
@hanzckernel
Copy link
Copy Markdown
Author

Addressed.

  • Probe codex --version before configuring Codex update hooks.
  • Codex >=0.124.0 uses hooks.json; older Codex keeps the legacy inline [[hooks]] path.
  • Unknown detection skips hook config instead of risking a broken config.
  • Added regression coverage for version selection, legacy fallback, and skip mode.

Verified: node --check bin/install.js, node --test tests/codex-config.test.cjs, git diff --check, npm test.

@trek-e trek-e added the area: hooks Session hooks, startup/stop label Apr 25, 2026
…n-migration

# Conflicts:
#	CHANGELOG.md
#	bin/install.js
Copy link
Copy Markdown
Collaborator

@trek-e trek-e left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Fix Codex hook installation to use hooks.json instead of inline TOML hooks

1. Peter Principle concern — 400+ lines of Codex internals from a first-time contributor

This PR adds: version detection via spawnSync, custom semver comparison, a partial TOML parser, hooks.json read/write/merge logic with path normalization, and a 6-variant command enumeration system for uninstall. That is the implementation surface of an experienced systems programmer who has reverse-engineered Codex's config format in depth. What is the source of the contributor's knowledge of Codex's hooks.json schema, version detection API, and migration behavior? Has any of this been validated against Codex's actual behavior, or is it reverse-engineered from observation?

2. Kernighan's Law: getManagedCodexUpdateHookCommands generates 6 variants because install wrote inconsistent paths

The uninstall function generates 6 command variants (normalized × portable base × 2 hook names × multiple quoting styles) to find what the install function wrote. This is a symptom, not a fix. The root cause is that the install path was written in an inconsistent format. The correct fix is: install writes ONE canonical path, uninstall removes that exact path. Enumerating all possible variants is self-documenting evidence that the install side was not fixed. When does the install path format diverge? Fix that, and the uninstall complexity collapses.

3. parseSimpleTomlStringAssignment is a partial TOML parser that fails silently

The function handles double-quoted and single-quoted strings. TOML also has: multi-line basic strings ("""), multi-line literal strings ('''), and escaped characters within strings (\", \\, \n, etc.). Adjacent keys in a real Codex TOML config may use any of these. When this parser encounters syntax it doesn't recognize, it returns null — silently. A caller that receives null from this function cannot distinguish "key not found" from "key found but unparseable." Silent null returns on parse failure will cause undetected migration failures.

4. detectCodexHookInstallMode conflates "Codex not installed" with "version check failed"

The function returns mode: 'skip' in both cases. On a machine where Codnet is installed but codex --version fails due to a PATH issue or permissions error, the GSD Codex hook is silently skipped — not installed, no error, no warning. The distinction between "Codex is not present" and "Codex is present but misconfigured" has different user-facing implications. One should skip silently; the other should warn.

5. GSD_CODEX_HOOKS_MODE env var is now a permanent production API surface

Adding an env var override to force production code into a test mode is a code smell. This env var will be discovered, documented (or not), and used by users who want to force a specific install behavior. You've created an undocumented knob with production side effects. This should be test infrastructure only — use dependency injection (pass installMode as a parameter) instead of an env var that bleeds into production.

6. The complexity-to-benefit ratio has not been justified

The old system had one failure mode: Codex rejected inline TOML hooks. The new system has: version detection failure, spawnSync unavailability, hooks.json read failure, hooks.json parse failure, hooks.json write failure, path normalization edge cases, partial TOML parse failure, and 6-variant uninstall mismatch. Gall's Law: a complex system that works must evolve from a simple system that worked. This PR replaced a simple system that had one known failure mode with a complex system that has eight. What evidence exists that the complex system handles all eight failure modes correctly?

VERDICT: CHANGES REQUIRED — fix the install path inconsistency at the root (eliminating the need for 6-variant uninstall), remove the production env var, and validate against actual Codex behavior.

@trek-e
Copy link
Copy Markdown
Collaborator

trek-e commented Apr 26, 2026

Closing this PR. A review identified more than 3 distinct problems with the implementation that collectively indicate it is not ready for merge in its current form. The specific issues are documented in the review comment above — please address all of them in a revised PR rather than pushing fixes to this branch. If you believe any finding is incorrect, open a new PR with a clear rebuttal for each point.

@trek-e trek-e closed this Apr 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: hooks Session hooks, startup/stop area: installer Installation, CLI setup bug Something isn't working needs changes Review requested changes that must be addressed review: changes requested PR reviewed — changes required before merge size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Codex 0.124.0 hook regression can leave GSD-installed Codex setups unable to start

2 participants