* fix(#4087): stage the hook helpers the Codex bundle's hooks require CODEX_HOOKS_TO_COPY is a flat, hand-maintained filename allowlist that never recursed, and Codex is excluded from installSharedHooksBundle() — the path that stages hooks/lib/ for full-bundle runtimes — by an !isCodex gate. Excluding hooks/lib/ was a correct scoped decision for #3579 until #3911 (2ea5efc15) gave gsd-context-monitor.js a real require('./lib/hook-exit.js'). From then on every fresh --codex install staged the hook without its helper and the hook died with MODULE_NOT_FOUND at module load, before its own try/catch, on every event Codex registers it for. The install still exited 0, so nothing surfaced it. Reproduced before changing anything, in a sandboxed CODEX_HOME: four hooks staged, no lib/, and the installed hook exiting 1 on "Cannot find module './lib/hook-exit.js'". Rather than hand-add today's three helpers — which re-breaks the next time a Codex-bundled hook grows a lib dependency, exactly how this regressed — the transitive-require walk already written for Cursor in 704859e9c is extracted out of writeCursorHooksJson into an exported stageTransitiveHookLibs(), Cursor is rewired onto it, and the Codex copy loop calls it. bin/install.js already required that module, so this adds no new seam. Cursor's staged set is byte-identical to base, compared file by file. Extraction surfaced a latent defect in that walker, fixed here: its regex read `./X` and `./lib/X` identically, but from a hook SCRIPT a bare `./X` is a sibling in hooks/ — gsd-check-update-worker.js requires `./managed-hooks-registry.cjs`, which is not a lib — so it demanded hooks/lib/managed-hooks-registry.cjs and the fail-loud guard threw. Seeds now match only `./lib/X`; lib files still match both, which is the sibling-within-lib case 704859e9c exists for. Cursor never exposed it because none of its scripts carries a bare sibling require. Three further grammar gaps closed after review, each in the fail-closed direction: an extensionless `require('./lib/x')` is valid CommonJS and was resolved literally, failing the install on a legitimate require — now resolved through .js/.cjs and written under its resolved name; a NESTED `./lib/sub/x.js` could not be expressed by the character class and was a SILENT miss, the one failure mode this function exists to remove — now refused loudly; and a capture carrying no alphanumeric character is prose, not a module name — hooks/lib/ injection-patterns.js documents this very mechanism with the literal string require('./lib/...'), which captured `...` and sent the resolver hunting for hooks/lib/... . The scan is still not comment-aware, which is disclosed at the call site rather than papered over. Seeded from the entries THIS invocation staged rather than probing the destination, so a file left by an earlier install whose source is no longer allowlisted cannot contribute helpers for a hook that is no longer shipped. The #3579 boundary holds: three of ten helpers ship, gsd-graphify-rebuild.sh among those correctly absent. Seven rows — three driving a real install into a sandboxed config dir (with HOME sandboxed for the child, since Codex's skills kind resolves from os.homedir() and the #3712 guard rightly refuses otherwise) and four pinning the discovery grammar directly. All proven fail-first; the set-equality row also reds on over-staging, which the count-based version it replaced did not catch. Fixes #4087 Fixes #4098 Emitted-Drift-Ack-Hash: hooks/lib/hook-exit.js — newly emitted for codex because the installer now stages the helpers its hooks require; the helper's own content is unchanged Emitted-Drift-Ack-Hash: hooks/lib/cli-exit.js — newly emitted for codex as hook-exit.js's transitive require; the helper's own content is unchanged Emitted-Drift-Ack-Hash: hooks/lib/exit-code-registry.js — newly emitted for codex as cli-exit.js's transitive require; the helper's own content is unchanged Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018FUAVz49BghqxoJgwt7EW9 * chore(#4087): add changeset Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018FUAVz49BghqxoJgwt7EW9 * fix(#4087): stage the hooks/lib helpers the Windsurf guards require Review of #4117, verified as asked and reproduced against a real install. Windsurf sets hostBehaviors.skipSharedHooksInstall, so like Cursor it never reaches installSharedHooksBundle -- the only other stager of hooks/lib -- and writeWindsurfHooksJson staged its two Cascade guards without the helpers both require at module load: gsd-windsurf-pre-write.js requires ./lib/hook-exit.js and ./lib/git-probe.js, gsd-windsurf-pre-command.js requires ./lib/hook-exit.js. stageTransitiveHookLibs had one call site, Cursor's. Measured on a fresh `--windsurf --global` install into a sandboxed HOME: the installer exited 0, hooks/ held only the two scripts and package.json, and executing either installed guard exited 1 with "Cannot find module './lib/hook-exit.js'" -- so every pre_write_code and pre_run_command event failed at load while the install reported success. The same command with `--cursor` staged four helpers and its hook ran, which is the control. Pre-existing rather than introduced here: at merge-base05092ff36the same three require lines exist and writeWindsurfHooksJson already staged no lib/, and this PR's diff carried no reference to Windsurf. Fixed here anyway because the helper this PR extracted is the right tool and a second runtime is a few lines onto it. writeWindsurfHooksJson now calls stageTransitiveHookLibs after staging its scripts, with the same gsd: -> gsd- transform the scripts receive, so a helper is rewritten the same way as its caller. The install-tree fixture regenerates with exactly hook-exit.js, git-probe.js, cli-exit.js and exit-code-registry.js added and no other fixture moved. Two rows execute the INSTALLED guards, beside the Codex rows they mirror; the existing windsurf-hooks-bridge rows run the guards from source and test behaviour, a different question, and are left as they are. Both new rows fail-first against the unfixed compiled artifact -- gsd-core/bin/lib, which is what bin/install.js loads -- on the MODULE_NOT_FOUND assertion. Emitted-Drift-Ack-Hash: hooks/lib/git-probe.js — first staged for Windsurf, whose pre-write guard requires it; the file itself is unchanged Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TadqrpTE2m6gCB7CaNNLcy --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
6 lines
1.3 KiB
Markdown
6 lines
1.3 KiB
Markdown
---
|
|
type: Fixed
|
|
pr: 4117
|
|
---
|
|
**Codex installs no longer ship a hook that cannot load** — with `--codex`, `gsd-context-monitor.js` was staged without the `hooks/lib/` helpers it requires, so it failed with a missing-module error at load, before its own error handling, on every event Codex registers it for. The install still reported success, so the only symptom was a Codex session erroring on each prompt. The helpers a Codex-bundled hook needs are now derived from what the staged scripts actually require, followed through helpers that require other helpers, rather than from a hand-maintained list that could not keep up: the same list had gone stale once already, which is how this broke. Helpers no Codex hook requires are still not shipped, and a hook whose helper is genuinely missing from the source now fails the install loudly instead of installing something that cannot run. Windsurf had the same gap, found in review: both Cascade guards require `hooks/lib/` helpers at load and a fresh `--windsurf` install staged neither, so every `pre_write_code` and `pre_run_command` event failed the same way. Windsurf is now wired onto the same derivation, and its installed guards are executed by the tests rather than only checked for existence. Full-bundle runtimes and Cursor are unaffected — Cursor's staged set is byte-identical. (#4087) (#4098)
|