{"version":"2.1.242","anchor":"plugins-can-ship-javascript-hook-modules-that-run-in-a-share","canonical_anchor":"plugins-can-ship-javascript-hook-modules-that-run-in-a-share","heading":"Plugins can ship JavaScript hook modules that run in a shared Bun worker, built but switched off","tier":"soon","area":"Plugins","scope":null,"heads_up":null,"url":"https:\/\/changelogs.core-directive.com\/v\/2.1.242\/e\/plugins-can-ship-javascript-hook-modules-that-run-in-a-share","release_url":"https:\/\/changelogs.core-directive.com\/v\/2.1.242","markdown":"### Plugins can ship JavaScript hook modules that run in a shared Bun worker, built but switched off\n\nPlugins could ship JavaScript hook modules running in a worker, but loading is refused here.\n\n**What**\n\nA plugin's `hooks\/hooks.json` may now name a JavaScript module that registers hooks in code rather than as shell commands, and that module runs in a background worker thread shared by all plugins, with crash containment, a heartbeat and a broad capability object passed in. Nothing loads in this build: the loader refuses with a message saying the rollout flag is off, the flag `tengu_plugin_hooks_modules` has a built-in value of false, and no plugin bundled with Claude Code declares a hooks module, so even a server-side switch would only enable third-party plugins.\n\n**Details**\n\n- The `modules` array in `hooks.json` names exactly one module per plugin, resolved relative to `hooks.json` and exporting a `register` function; a second entry is refused.\n- The worker's entry point is baked into the binary as a build constant pointing at an internal hooks worker script, falling back to a relative `hooks-worker.ts` path when that constant is missing.\n- Host and worker speak a framed protocol covering load, dispatch, build, call, ping and flush, and the host heartbeats the worker.\n- A hook that spins without yielding, or a plugin that repeatedly ignores its abort signal, causes the worker to be respawned and that plugin to be unloaded; repeated crashes that cannot be pinned on one plugin switch hook modules off for the rest of the session.\n- Setting the environment variable `CLAUDE_CODE_HOOKS_SAME_THREAD` runs hooks in the main process instead of the worker.\n- Each module receives a `$` object of named capabilities: `$.ui` with ask, toast, status, notice, log and resolve; `$.model` with complete plus classify, which builds its own classifier prompt and maps the reply back to a label; `$.store` with get, set, delete and keys under a character limit; `$.clock` with sleep, after and every returning cancellable timers; `$.fs` with readFile, writeFile, listDir, exists and stat; plus `$.http.fetch`, `$.mcp.call`, `$.tool.register\/list\/call`, `$.agent.spawn`, `$.prompt.submit` and `$.audio.play\/speak`.\n- Arguments are validated per capability and errors name the offending plugin; tool schemas and interface text have size limits, and a hook can claim work whose promise nobody is waiting on.\n- Telemetry is already wired for `plugin_function_hooks_load`, `plugin_function_hooks_worker` and `plugin_function_hooks_register_tool`, with respawns recorded on the worker event and a plugin unloaded by a crash recorded on the load event as \"crashed_worker\".\n- The same flag also reveals a plugin-types row in the doctor output and in the plugin dialog listing.\n\n**Evidence**\n\n`tengu_plugin_hooks_modules`, `hooks worker spawned (one for every plugin)`, `hooks.json `modules` names one hooks module per plugin; a second entry is refused`, `$.audio.play with loop needs options.signal: the clip repeats until it aborts`\n\n- Area: Plugins\n- Names: `CLAUDE_CODE_HOOKS_SAME_THREAD`\n- Tier: Nothing to try yet\n- Useful: 3\/5\n- Signal: 5\/5\n- Present in the build but not switched on"}