Faster warm dispatch for import-safe fleet hooks via a CJS rolldown bundle plus a V8 compile-cache loader. Hook sources stay .mts.
scripts/fleet/make-hook-dispatch.mtsis the maker. It scans.claude/hooks/fleet/for hooks that are both import-safe (entrypoint-guarded) and export arun(payload)entry, then writes.claude/hooks/fleet/_dispatch/dispatch-table.mts. That file is a STATIC table ofpathtothunk(one staticimport()per hook) that rolldown can see and bundle. A dynamicimport(path.join(HOOKS_DIR, rel))can't be statically bundled, so the static table is what makes the dispatcher bundle-able.scripts/fleet/build-hook-bundle.mtsplus.config/repo/rolldown/hook-bundle.config.mtsis the build. Rolldown bundles the dispatcher, the generated table, every referenced hook,_shared/, and only the used slices of@socketsecurity/lib-stableinto.claude/hooks/fleet/_dispatch/bundle.cjs. Output is CJS format, minified, with no source maps and no.d.ts, tree-shaken, and heavy unreachable lib subgraphs stubbed viacreateLibStubPlugin..claude/hooks/fleet/_dispatch/index.cjsis the hand-written thin loader (plain CJS, NOT bundled). It callsrequire('node:module').enableCompileCache(<repo>/node_modules/.cache/fleet-hooks)thenrequire('./bundle.cjs'), forwarding the event arg (process.argv[2])..claude/hooks/fleet/_dispatch/dispatch.mtsis the dispatcher. It reads the event arg and stdin once, runs the trigger pre-flight, looks up the matching hooks in the static table, and runs each hook's exportedrun(payload)with early-exit on the first blocking decision.
V8's compile cache (module.enableCompileCache) reliably caches and auto-flushes plain CJS modules on normal process exit. A type-stripped .mts dispatcher did NOT auto-flush. A normal exit left ZERO cache files on disk, so every spawn recompiled from scratch. Emitting a plain CJS bundle is the core rationale. The loader stays CJS, the bundle is CJS, and the compile cache actually persists between spawns.
Most of a cold hook spawn (~1s) is Node STARTUP (process create plus runtime init), with an idle baseline near 100ms. That is fixed cost the bundle and cache cannot touch. The compile cache only removes module-COMPILE time on warm spawns. The real process-count win comes from collapsing many per-hook node spawns into one dispatcher spawn per event. The bundle plus cache is the secondary compile-time win on top. Do not overclaim a blanket "Nx faster hook" number.
Only hooks that are entrypoint-guarded (import.meta.url matches process.argv[1]) and export a run(payload) are eligible. Importing them must not fire main() and must not call process.exit(), which would tear down the shared dispatcher for every hook. The maker skips the rest. The dispatch path is wired in but NOT yet repointed in settings.json. The per-hook node .../index.mts invocations still run unchanged, so the bundle is an additive, opt-in fast path until every targeted hook is converted. This is the disabled-seam pattern: the wire-in point is present and tested, the cutover is gated off.
bundle-stale-reminder (PostToolUse, Edit|Write) fires after an edit to the dispatcher, the dispatch table, a bundled hook source, or _shared/, and reminds you to rebuild. It never blocks. Rebuild with:
node scripts/fleet/build-hook-bundle.mtsThe bypass phrase is registered in docs/agents.md/fleet/bypass-phrases.md under the hook-bundle-current row (the canonical-phrase grammar).
test/unit/fleet/hook-bundle-compile-cache.test.mts (vitest) builds the bundle, spawns the .cjs loader for an event, then asserts the compile-cache dir is populated under <cache>/<v8-version>/ (cache files greater than 0). Without that file count the cache claim is unproven, so the test is the gate on the whole feature.