/** * This instance's state object, for a host to hand to another instance. * @returns {HookIoSharedState} */ export function hookIoSharedState(): HookIoSharedState; /** * Route every slot of {@link HookIoSharedState} on this instance through `state`. * * A host that ships its OWN hook-io module beside the packaged hooks ends up * with two instances of this file's state in one process, each with its own * registry and its own latches. Without this seam the host configures each slot * twice, and a slot it sets on only one instance is invisible to the readers on * the other. For the registry that means those readers resolve a specifier at * RUNTIME, inside a bundle with no node_modules, and the gate fails closed on * every call. Adopting one state object removes that failure mode rather than * policing it. * * WHY AN API AND NOT A BUNDLER ALIAS: collapsing the two module records at build * time (an esbuild `alias` from the host's module to this one) is the cheaper * fix and needs no API — but it works only when the host's module is a COPY of * this one. The motivating host's is not: it exports names this module does not * have and lacks names this one exports, so an alias breaks every call site of * the difference. `test/claude-hooks-exports.test.mjs` still states the rule for * a true copy — share this module, never duplicate it. This seam serves the * other case, a host with its own module that must agree with ours on state. * * Call it before importing any module that reads a slot, for the same reason * {@link registerLazyModules} carries that rule: a reader binds at its own * module scope. * * Slots already set on EITHER side survive: this instance's registrations and * latches carry over, and a value already present in `state` wins, since it is * the adopted root's own choice. Adopting the state this instance already holds * is a no-op. * * CONSTRAINT: every instance must adopt the same root object, and none may adopt * a second, different one. `shared` is reassigned, so a later `B.adopt(C)` would * leave an earlier `A.adopt(B)` pointing at an abandoned object whose readers * nothing reaches. This is not enforced with a throw: callers are bundle entry * points, and a throw at their top level kills the hook before it writes a * response — a hook that emits nothing reads as non-blocking, which is the * fail-OPEN this whole module is built to avoid. * @param {HookIoSharedState} state * @returns {void} */ export function adoptHookIoSharedState(state: HookIoSharedState): void; /** * True when this module is the process entry point (run directly as a CLI, not * imported). Guards an undefined `process.argv[1]` (e.g. the REPL) before * resolving it: the bare `import.meta.url === pathToFileURL(process.argv[1])` * form throws there. Resolving argv[1] through pathToFileURL also normalizes a * relative invocation path to an absolute file URL before comparing. * @param {string} importMetaUrl the caller's `import.meta.url` * @returns {boolean} */ export function isMain(importMetaUrl: string): boolean; /** * Claim the process's CLI-entry slot for the calling module: every subsequent * {@link isMain} call answers false. For bundle entry points that inline other * isMain-guarded hooks (see isMain's bundle note); a claim cannot be released. * @returns {void} */ export function claimCliEntry(): void; /** * Find a `--name=value` flag in argv (by prefix scan, not position) and return * its value, or undefined if absent. A named flag stays correct when unrelated * arguments are prepended or interspersed — a bare positional index (argv[2]) * silently reads the wrong value the moment the command line grows. * @param {string[]} argv * @param {string} name flag name without the leading `--` or trailing `=` * @returns {string|undefined} */ export function readFlag(argv: string[], name: string): string | undefined; /** * Whether hook failures pass the guarded action through. True unless the caller * explicitly asked for the closed posture. * * The accepted opt-out spellings are a SET rather than the single exact `"1"` * the AGENT_SANITIZER_*_DISABLED knobs use, because the direction of the * mistake is reversed: those default to the safe side, so an unrecognized value * there costs nothing, while here it leaves an operator who asked for * strictness without it. `"false"` is the one spelling reached for by reflex, * so it is honored; everything else (`""`, `"no"`, `"off"`) is the open posture. * @param {NodeJS.ProcessEnv | Record} [env] * @returns {boolean} */ export function failOpenEnabled(env?: NodeJS.ProcessEnv | Record): boolean; /** * The hook names an operator switched off, restricted to `known`. * * An unrecognized name is REPORTED and dropped rather than thrown on, and the * direction of that choice is the point: this variable is set outside the * session, so a throw here — or a blocking verdict — would leave every tool * call failing on a config value the session cannot edit. Dropping it keeps the * named hook running, which is the safe side, and `report` is what stops it * being silent. * @param {readonly string[]} known every dispatchable hook name * @param {NodeJS.ProcessEnv | Record} [env] * @param {(message: string) => void} [report] * @returns {Set} */ export function disabledHooks(known: readonly string[], env?: NodeJS.ProcessEnv | Record, report?: (message: string) => void): Set; /** * The model-facing warning accompanying a fail-open pass-through. Emitted as * `additionalContext` so the transcript still carries the failure: the posture * gives up ENFORCEMENT, not visibility, and stdout is never left empty (which * Claude Code would record as a clean run rather than a degraded one). * * The reinstall remedy rides along when the failure looks like an unloaded * binding. A `DEP_UNAVAILABLE` error already carries its remedy in the message * `safeErrMessage` splices in below, but the bare TypeError V8 raises for an * undefined binding names neither the package nor the fix — and under this * posture there is no permissionDecisionReason carrying one either, so the only * message telling a reader how to un-break the install would be the one that * went missing. `failedPackages`/`packageMessage` are injectable for the same * reason `depLoadHint`'s are: the recorded-failure set is process-wide and * untestable otherwise. * @param {string} hookName * @param {string} guarded what passed through, e.g. "tool output" * @param {unknown} err * @param {() => string[]} [failedPackages] * @param {(pkg: string) => string} [packageMessage] * @returns {string} */ export function failOpenContext(hookName: string, guarded: string, err: unknown, failedPackages?: () => string[], packageMessage?: (pkg: string) => string): string; /** * The byte length recorded by the most recent {@link readStdinJson} call, or * null if none has run yet (e.g. a test injected its own `readInput`). Read by * `runJudgeCli` to fold the payload size into the slow-hook notice. * @returns {number | null} */ export function lastStdinByteLength(): number | null; /** * @param {number} [maxBytes] cap before aborting (overridable for tests) * @returns {Promise} */ export function readStdinJson(maxBytes?: number): Promise; /** * Register already-loaded module namespaces for {@link lazyImport} to return in * place of a runtime dynamic import. Call before importing any module that * lazy-loads the given specifiers. * @param {Record>} modules specifier → namespace * @returns {void} */ export function registerLazyModules(modules: Record>): void; /** * The pre-registered namespace for `specifier`, or undefined when none was * registered. The synchronous face of the registry, for call sites that cannot * await {@link lazyImport} (e.g. a sync callback binding a scanner package): * inside a bundle the registered namespace is the ONLY way to reach the * package, since a runtime require/import has no node_modules to resolve from. * @param {string} specifier * @returns {Record | undefined} */ export function registeredLazyModule(specifier: string): Record | undefined; /** * Dynamic-import `specifier`, yielding `{}` when the module cannot be loaded. * Hooks bind their npm packages through this instead of a bare static import: a * static npm import resolves before any try/catch, so a missing node_modules * would crash the hook at load — the harness treats that as a non-blocking * error and the tool call proceeds UNGUARDED (fail OPEN). Destructuring from * the `{}` failure value leaves each binding undefined, so the first use throws * into the hook's own catch and the hook takes its declared failure posture * instead. A specifier registered via {@link registerLazyModules} resolves from * the registry without touching the loader. * @param {string} specifier * @returns {Promise>} */ export function lazyImport(specifier: string): Promise>; /** * The most recently recorded load error for `pkg` under any of its specifiers — * the bare package or a subpath export (`pkg/output`, `pkg/invisible`) — or * undefined when none is recorded. Hooks import a package through several * subpaths; any one of them names why the package is absent, and the newest * record reflects the current failure when they differ. * @param {string} pkg * @returns {unknown} */ export function lazyImportErrorFor(pkg: string): unknown; /** * Package names (never relative-path specifiers) with a recorded load error, * newest first — so a fail-closed reason can name whichever dependency actually * failed instead of consulting a hardcoded package list. * @returns {string[]} */ export function failedLazyPackages(): string[]; /** * Adopt a host's own remedy as the default {@link missingPackageMessage} and * {@link missingPackageError} state when their caller passes none. This refusal * to hard-code the wording is what prevents a fail-closed reason whose remedy * names a command the host doesn't have: deep call sites (controlPlane's * missing-package throw) never take a remedy argument, so without this seam * they can only ever tell a reader to run `pnpm install` — wrong advice in a * host whose one install entry point is its own setup script. An explicit * per-call remedy still wins over the configured one. Unlike the hookgate * marker there is no too-late window: the remedy is consulted at each throw, * never resolved at module scope, so a later call steers every later message. * Keep it to a sentence — a remedy beyond ~260 characters overruns the 300-char * message budget (see missingPackageMessage). * @param {string | null} remedy host remedy text, or null to restore the package default * @returns {void} */ export function configureMissingPackageRemedy(remedy: string | null): void; /** * The fail-closed reason for a package a hook could not load: the recorded * loader error plus the remedy. The cause is scrubbed (it is spliced into * reasons shown to user and model) and its cap is COMPUTED so that * prefix + cause + remedy always fits the downstream 300-char safeErrMessage * re-scrub — the remedy can never be truncated off, whatever the package name. * A remedy that alone exceeds that budget (roughly 260 characters) leaves the * cause nothing to spend and still overruns; keep host remedies to a sentence. * @param {string} pkg * @param {unknown} [err] * @param {string} [remedy] * @returns {string} */ export function missingPackageMessage(pkg: string, err?: unknown, remedy?: string): string; /** * {@link missingPackageMessage} as a throwable, tagged `code: "DEP_UNAVAILABLE"` * so downstream reason-builders can recognize it structurally and not append a * second copy of the same cause. * @param {string} pkg * @param {unknown} [err] * @param {string} [remedy] * @returns {Error} */ export function missingPackageError(pkg: string, err?: unknown, remedy?: string): Error; /** * A monotonic wall-clock budget shared across one hook run's downstream blocking * calls. `remainingMs()` returns the milliseconds left until the budget is spent * (clamped at 0), so an orchestrator hands each sub-call `min(its own timeout, * remaining)` and a SERIES of daemon calls can never sum past the budget. This is * the fail-open hazard a per-call-only deadline leaves open: when many output * leaves each pay the Layer-4 redactor, the calls' individual timeouts bound each * call but not their SUM — a pathological pile-up could exceed the PostToolUse * hook kill, and a killed hook is non-blocking, so the RAW output would be shown. * `now` is injectable so time-dependent logic is unit-testable with a fake clock. * @param {number} budgetMs total wall-clock budget from creation * @param {() => number} [now] clock source (defaults to Date.now) * @returns {{ remainingMs: () => number }} */ export function makeDeadline(budgetMs: number, now?: () => number): { remainingMs: () => number; }; /** * Scrub untrusted text before it is spliced into the model's context via a * warning/reason field: strip ANSI and payload-capable invisibles to a fixed * point (via the injected `layer1`), then cap by whole code points. `layer1` is * injected rather than imported so this dependency-light module never eagerly * loads the sanitizer package — each caller passes its own caught-import * binding. * * Pass a layer1 that also normalizes lone surrogates — `applyLayer1WellFormed`, * never the bare `applyLayer1`. The model's UTF-16 context must be well-formed, * and the code-point cap below must not slice a half it mistook for a whole * character. This module used to re-spell that substitution over a private * regex; the package now owns the one definition. * @param {unknown} raw * @param {(text: string) => { cleaned: string }} layer1 MUST normalize lone surrogates * @param {number} [cap] * @returns {string} */ export function scrubUntrustedText(raw: unknown, layer1: (text: string) => { cleaned: string; }, cap?: number): string; /** * Message from a caught value, which is `unknown` under strict mode. Appends * the cause chain (one level) when the cause is itself an Error so callers * get "outer: root" instead of just "outer" when an error wraps another. * @param {unknown} err * @returns {string} */ export function errMessage(err: unknown): string; /** * errMessage() for an error whose message may embed attacker-chosen bytes: V8 * quotes a snippet of the offending input in a JSON.parse SyntaxError, so a hook * that splices errMessage(err) into a user-/model-facing reason would relay raw * ANSI escapes and invisible/format characters lifted from that snippet. Keep only * printable ASCII (plus tab/newline) and drop every other code point — dropping the * ESC/CSI-introducer and zero-width bytes neutralizes the sequence while leaving the * residual literal text readable — then cap the length so a long snippet can't flood * the reason. Use this instead of errMessage at any callsite that splices the * message into a reason/warning shown to the user or model. * @param {unknown} err * @param {number} [cap] * @returns {string} */ export function safeErrMessage(err: unknown, cap?: number): string; /** * Write the `hookSpecificOutput` envelope a hook returns to stdout. * @param {string} hookEventName * @param {Record} fields * @returns {void} */ export function emitHookResponse(hookEventName: string, fields: Record): void; /** * Adopt a host's own cold-start marker path in place of the derived one, so a * host whose setup script already writes a marker under its own convention can * use these hooks without running a second, disagreeing wait loop against a path * nothing writes. * * ORDERING, same rule as {@link registerLazyModules}: call this before importing * any hook module. `lib/control-plane.mjs` resolves the marker at MODULE scope, * so a call that lands after that import cannot reach the wait it was meant to * steer. A late call is reported on stderr rather than thrown: a throw at a * bundle entry's top level kills the hook process before it writes a response, * and a hook that emits nothing is read as non-blocking — the fail-OPEN this * whole file is built to avoid. The late call still takes effect for every * later resolution. * @param {string | null} path absolute marker path, or null to restore the derivation * @returns {void} */ export function configureHookgateMarker(path: string | null): void; /** * Path of the cold-start in-flight marker a host's setup script writes * SYNCHRONOUSLY before it starts installing deps (its own PID as the contents) * and removes once the hook dependencies are provisioned. A hook that fires * before setup finishes finds the marker and WAITS for its dependency rather * than failing closed on it — so the first turn is merely delayed, never * blocked, for as long as setup is still alive (the PID lets the hook tell a * live install from a stale marker left by a killed setup). Derived purely from * the raw CLAUDE_PROJECT_DIR the harness sets for both processes (no * canonicalization — the two must produce byte-identical paths), so no env has * to propagate from setup to the hook. Null when CLAUDE_PROJECT_DIR is unset (no * setup ran → nothing to wait on), or whatever a host set via * {@link configureHookgateMarker}. * @param {string | undefined} [projectDir] * @param {string | undefined} [runtimeDir] * @returns {string | null} */ export function hookgateMarkerPath(projectDir?: string | undefined, runtimeDir?: string | undefined): string | null; /** * Is a setup process DEMONSTRABLY running right now? * * The strict twin of {@link probeSetupAlive}, and the two differ only in which * way they fall when the evidence runs out. That one decides whether to keep * WAITING for a dependency, so every ambiguity — an unreadable marker, an * unparseable pid, no project dir — reads as alive: waiting a moment longer is * cheap and giving up early fails a hook closed. This one decides whether to * stop charging a caller for time it spent, so the same ambiguity must read as * NOT running: an absence of evidence that the machine was busy is not evidence * that it was, and discounting a wait on that basis would hide the very * slowdowns the caller is measuring for. * * Positive evidence is one of two things: the setup lock is held (the kernel * drops an flock the instant its holder dies, so held means running), or the * marker's pid names a live process. A marker this uid does not own is not * evidence about our setup at all. * @param {string | null} markerPath * @returns {boolean} */ export function setupRunning(markerPath: string | null): boolean; /** * Is the setup process that wrote `markerPath` still alive? * * A marker declaring {@link SETUP_LOCK_DECLARATION} is judged by the LOCK, and that * answer has no aliasing: the kernel drops an flock the instant its holder dies, so * held means alive and free means dead, with no third state and nothing to reuse. A * marker that declares nothing carries only a pid, and `process.kill(pid, 0)` is all * there is — it throws ESRCH once the process is gone (a killed setup → stale * marker, so stop waiting) and EPERM when it exists but is not ours (still alive). * That reading is the one this replaces where it can: a recycled pid reads as a live * setup for as long as the caller's ceiling allows. * * An unreadable / not-yet-written marker is treated as alive — favouring a brief * wait over a premature give-up during setup's write race. A null markerPath (no * project dir → no setup to wait on) reads as alive so the caller's own * grace/ceiling bound governs. * @param {string | null} markerPath * @returns {boolean} */ export function probeSetupAlive(markerPath: string | null): boolean; /** * Resolve a lazily-loaded dependency, blocking through the cold-start window while * setup is still installing it. Returns the loaded value, or null once it gives up * (the caller leaves its bindings undefined so the hook fails closed). It waits for * as long as setup is genuinely alive, so a slow install is never cut off; the only * bound on that wait is a backstop ceiling that stays under the hook's harness * timeout — a hook killed for running over is a fail-OPEN, the opposite of what a * gate wants. The give-up cases are the honest ones (setup finished/died without the * dep, or no setup at all), so a genuinely-absent dep fails closed fast, never after * a long block: * - import succeeds → return immediately (warm session: no wait). * - marker present AND setup alive → setup is working; wait it out (ceilingMs is a * backstop only, for a hung-but-alive setup). * - was installing, now not (marker cleared, or a stale marker from a killed setup) * → settleMs grace for a just-orphaned install to * land, then give up: the dep is absent. * - no live setup ever seen → wait only graceMs (tolerating setup not having * written the marker yet), then give up. * @param {{ * tryImport: () => Promise, * markerPresent: () => boolean, * setupAlive: () => boolean, * now?: () => number, * sleep?: (ms: number) => Promise, * graceMs?: number, * settleMs?: number, * ceilingMs?: number, * intervalMs?: number, * }} deps * @returns {Promise} */ export function awaitLazyDependency({ tryImport, markerPresent, setupAlive, now, sleep, graceMs, settleMs, ceilingMs, intervalMs, }: { tryImport: () => Promise; markerPresent: () => boolean; setupAlive: () => boolean; now?: () => number; sleep?: (ms: number) => Promise; graceMs?: number; settleMs?: number; ceilingMs?: number; intervalMs?: number; }): Promise; /** * Is the file at `path` one WE wrote — a regular file owned by this uid — rather * than a squat? These markers live at predictable, world-visible $TMPDIR paths, so * a co-tenant could pre-plant a file (or a symlink at the path) to steer a gate. * lstatSync does NOT traverse a final symlink, so a planted symlink reads as a * symlink (isFile() false) and a foreign file fails the uid check: either way the * marker is untrusted and the caller ignores it. * @param {string | null} path * @returns {boolean} */ export function markerIsTrusted(path: string | null): boolean; /** * Create a presence sentinel at `path` without following a symlink a co-tenant * may have pre-planted there. These sentinels live at predictable, world-visible * paths under $TMPDIR (a project-hash or fixed name), so a plain writeFileSync — * which opens O_CREAT|O_TRUNC and follows a symlink at the path — would let * anyone able to plant that symlink redirect the write and truncate an arbitrary * file the hook's user owns. Unlink any existing entry first (removing a squatted * symlink), then create exclusively (O_EXCL) so a symlink re-planted in the race * window fails the open rather than being dereferenced. Content is irrelevant — * callers test only for existence — so the file is left empty. Best-effort: a * missing/read-only $TMPDIR or a lost race just leaves the sentinel absent, and * every caller treats "absent" as "not yet done" (a repeated ask, never a crash), * so all failures are swallowed. * @param {string} path * @returns {void} */ export function writeSentinelFile(path: string): void; /** * Write `content` to `path` without following a symlink a co-tenant may have * pre-planted there — the content-bearing counterpart to writeSentinelFile. These * hooks write to predictable, world-visible $TMPDIR paths (a project-hash name, or * a content-addressed digest an attacker who chose the input bytes can precompute), * so a plain writeFileSync — which opens O_CREAT|O_TRUNC and follows a final * symlink — would let anyone able to plant that symlink redirect the write and * truncate/overwrite an arbitrary file the hook's user owns. Unlink any existing * entry first (removing a squatted symlink), then create exclusively (O_EXCL via * "wx") so a symlink re-planted in the unlink→open race window fails the open * rather than being dereferenced. Returns true on success, false when the write * could not be completed (unwritable dir, or a lost race) so the caller decides * whether a failed best-effort write is fatal. * @param {string} path * @param {string} content * @param {number} [mode] * @returns {boolean} */ export function writeFileNoFollow(path: string, content: string, mode?: number): boolean; /** Claude Code hook event names (the hookEventName field). */ export const HookEvent: Readonly<{ PRE_TOOL_USE: "PreToolUse"; POST_TOOL_USE: "PostToolUse"; USER_PROMPT_SUBMIT: "UserPromptSubmit"; SESSION_START: "SessionStart"; INSTRUCTIONS_LOADED: "InstructionsLoaded"; }>; /** Claude Code permissionDecision verdicts. */ export const PermissionDecision: Readonly<{ ALLOW: "allow"; DENY: "deny"; ASK: "ask"; }>; /** * The public knob over the hooks' INFRASTRUCTURE failure posture. Installed as * Claude Code hooks these fail OPEN by default — a hook that could not run lets * the guarded action through with a loud warning rather than blocking the * session on its own breakage. Setting it to `"0"` restores the fail-CLOSED * posture (block/ask/suppress). * * The knob covers the hooks' own failures ONLY. What a working sanitizer * DECIDED is untouched by it, and so is the {@link failClosedFields}-style * wiring a downstream host does directly — a host that wants strictness gets it * by construction, not by remembering to set an env var. */ export const FAIL_OPEN_ENV: "AGENT_SANITIZER_FAIL_OPEN"; /** * Values that turn the default posture back to fail-closed. Matched exactly: * a case-insensitive match would need `tr`, which the launcher cannot reach * (it runs its no-node arm on shell builtins alone). * * THE SINGLE SOURCE OF TRUTH for the closed set. The shell shims cannot import * it, so `plugin/scripts/lib/fail-open.sh` is GENERATED from it by * `scripts/gen-fail-open-lib.mjs` and committed; the round trip is asserted in * plugin/test/fail-open-parity.test.mjs. Everything else that spells the set * out by hand is an implementation that must appear in the parity table in * tests/test_safe_launch.py. */ export const FAIL_CLOSED_VALUES: readonly string[]; /** * The public knob that turns individual hooks off: a comma-separated list of * hook names (the `--hook=` modes). The layer opt-outs * (`AGENT_SANITIZER_*_DISABLED`) narrow what a hook rewrites; this one is for * the deployment that wants a whole event unguarded — a session whose prompts * legitimately carry escape sequences, or one where the SessionStart context * scan is redundant because the instruction files are already vetted upstream. * Without it the only way to drop one hook is to edit the shipped hooks.json, * which the next plugin update overwrites. */ export const DISABLED_HOOKS_ENV: "AGENT_SANITIZER_DISABLED_HOOKS"; /** * Hard cap on hook stdin. A well-formed Claude Code hook payload is at most a * few MB (tool input plus the harness-truncated tool output); 64 MiB leaves * generous headroom while refusing a runaway or malformed sender before its * bytes are buffered into memory — an unbounded read would OOM the hook process * and take its own fail-closed output down with it. */ export const MAX_STDIN_BYTES: number; /** * A hook run that received no event at all: stdin closed with zero bytes. Its * own type, not the `SyntaxError` an empty string gets from `JSON.parse`, * because the two are different faults with different fixes — a malformed * payload is a sender that sent something wrong, an empty one is a hook wired to * a channel that sent nothing, and a caller that cannot tell them apart reports * the wrong cause and offers remedies for content it never received. */ export class EmptyStdinError extends Error { constructor(); } /** * The remedy {@link missingPackageMessage} states when the host does not supply * one of its own. A host whose install has a specific entry point (a setup * script, a devcontainer rebuild) passes that instead, so the reason names the * command the reader should actually run. */ export const DEFAULT_MISSING_PACKAGE_REMEDY: "reinstall the hook dependencies (pnpm install) and retry."; /** * The project the hooks are guarding. Every per-project $TMPDIR store is keyed to * it, so it lives here — beside the other shared identity these hooks agree on — * rather than in whichever store happened to need it first. */ export const PROJECT_DIR: string; /** Short project digest keying this project's $TMPDIR store names. */ export const PROJECT_HASH: string; /** * The line a cold-start marker carries on its own to declare that its writer holds * an exclusive `flock` on the marker file for the whole install. * * This is the marker's PROTOCOL, and the reason it is declared in the data rather * than assumed: a reader cannot tell "the writer released the lock because it died" * from "the writer never took one" by looking at a free lock, so only a marker that * says it locks may be judged by the lock. */ export const SETUP_LOCK_DECLARATION: "flock"; /** * EVERY process-wide slot these helpers keep — the four a host can observe or * steer, so a second instance that adopts this object is steered in all four at * once. A slot left off this object is one a host must configure per instance, * and forgetting the second call fails silently; that is the whole failure class * {@link adoptHookIoSharedState} exists to remove, so the object is complete * rather than covering only the registry. * * - `lazyModules` — the namespaces {@link lazyImport} answers from. Empty when * the hooks run from source; a build-time BUNDLE (which ships with no * node_modules for the runtime `import()` to resolve) statically imports its * packages and registers them here before importing the hooks that lazy-load * them, so the same hook source runs unchanged in both worlds. * - `cliEntryClaimed` — the CLI-entry latch {@link isMain} reads. * - `missingPackageRemedy` — the host remedy {@link configureMissingPackageRemedy} * sets; null keeps {@link DEFAULT_MISSING_PACKAGE_REMEDY}. * - `hookgateMarker` — the marker path {@link configureHookgateMarker} sets, and * `hookgateMarkerResolved`, the latch that makes a too-late call say so. */ export type HookIoSharedState = { lazyModules: Record>; cliEntryClaimed: boolean; missingPackageRemedy: string | null; hookgateMarker: string | null; hookgateMarkerResolved: boolean; };