Plugins could ship JavaScript hook modules running in a worker, but loading is refused here.
The whole worker-based plugin hook module runtime is built; tengu_plugin_hooks_modules defaults false and the loader says so.
What's wrong with this entry?
A 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.
- The
modulesarray inhooks.jsonnames exactly one module per plugin, resolved relative tohooks.jsonand exporting aregisterfunction; a second entry is refused. - 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.tspath when that constant is missing. - Host and worker speak a framed protocol covering load, dispatch, build, call, ping and flush, and the host heartbeats the worker.
- 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.
- Setting the environment variable
CLAUDE_CODE_HOOKS_SAME_THREADruns hooks in the main process instead of the worker. - Each module receives a
$object of named capabilities:$.uiwith ask, toast, status, notice, log and resolve;$.modelwith complete plus classify, which builds its own classifier prompt and maps the reply back to a label;$.storewith get, set, delete and keys under a character limit;$.clockwith sleep, after and every returning cancellable timers;$.fswith readFile, writeFile, listDir, exists and stat; plus$.http.fetch,$.mcp.call,$.tool.register/list/call,$.agent.spawn,$.prompt.submitand$.audio.play/speak. - 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.
- Telemetry is already wired for
plugin_function_hooks_load,plugin_function_hooks_workerandplugin_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". - The same flag also reveals a plugin-types row in the doctor output and in the plugin dialog listing.
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
Strings lifted out of the shipped bundle, so the claim above can be checked against them.