import type { Logger } from 'winston'; import type { HookRegistry } from './HookRegistry'; import type { HookInput, AggregatedHookResult } from './types'; /** Default per-hook timeout when a matcher doesn't set its own. */ export declare const DEFAULT_HOOK_TIMEOUT_MS = 30000; /** * Options for a single `executeHooks` call. The `input` drives everything — * the event name is read from `input.hook_event_name`, matchers are looked * up against that event, and each hook receives `input` directly. */ export interface ExecuteHooksOptions { registry: HookRegistry; input: HookInput; /** Scope lookup to this session (in addition to global matchers). */ sessionId?: string; /** Query string matched against each matcher's pattern (tool name, etc.). */ matchQuery?: string; /** Parent AbortSignal — combined with per-hook timeout into the hook signal. */ signal?: AbortSignal; /** Default per-hook timeout; overridden by `matcher.timeout` when present. */ timeoutMs?: number; /** Optional winston logger for non-internal hook errors. */ logger?: Logger; } /** * Fires every matcher registered against `input.hook_event_name`, folding * their results per `deny > ask > allow` precedence and accumulating * context/errors. * * ## Parallelism and determinism * * All matching hooks fire simultaneously and are awaited via `Promise.all`, * which preserves input-array order in its returned results. The fold * therefore iterates outcomes in **registration order** — outer loop over * matchers as they sit in the registry (global first, then session), inner * loop over each matcher's `hooks` array. Last-writer-wins fields * (`updatedInput`, `updatedOutput`) are deterministic in that order, even * though hooks may complete in arbitrary wall-clock order. * * Consumers that need a single authoritative rewrite should still scope * `updatedInput`/`updatedOutput` to one hook per matcher to avoid subtle * precedence bugs when matchers are added in a different order than * expected. * * ## Timeouts and cancellation * * Each matcher receives **one shared `AbortSignal`** derived from the * caller's parent signal combined with `matcher.timeout` (falling back to * `opts.timeoutMs`, default {@link DEFAULT_HOOK_TIMEOUT_MS}). Sharing the * signal across hooks in a matcher collapses N timer allocations into * one, which matters on the PreToolUse hot path where a matcher with * several hooks fires on every tool call. Each hook call is raced * against the shared signal, so even a hook that ignores the signal is * force-unblocked when the timeout fires. Timeout/abort errors are * swallowed into the aggregated result's `errors` array (non-fatal by * default). * * ## Internal matchers * * A matcher with `internal: true` is excluded from both the `errors` array * and the logger output. Use it for infrastructure hooks whose failures * should not pollute user-visible diagnostics. * * ## Once semantics — atomic at-most-once * * A matcher with `once: true` is removed from the registry **before any * hook runs**, inside the synchronous prefix of `executeHooks` (between * `getMatchers` and the first `await`). Because Node's event loop serialises * sync work, two concurrent `executeHooks` calls can never both observe * and dispatch the same `once` matcher — whichever call runs its sync * prefix first consumes it, and the loser sees an empty bucket. * * Trade-off: if every hook in a `once` matcher throws, the matcher is * still gone. "Once" here means "at most one dispatch, ever", not "at * most one successful execution with retry on failure". Hosts that need * retry semantics should register a normal matcher and self-unregister * via the `unregister` callback returned from `registry.register`. */ export declare function executeHooks(opts: ExecuteHooksOptions): Promise;