/** * A shell command as a plugin hook. * * The lifecycle manager carries a hook system — `pre_tool_use`, * `post_tool_use`, `turn_start`, `turn_end` and the rest — and `registerHook` * lets a host attach a handler without a plugin on disk. What every host then * writes for itself is the same adapter: run a command, hand it the event, * read its exit code. This file is that adapter, once, so an operator * application, an ACP server and an embedder all offer the same contract and * an operator's hook script works under all three. * * ## The contract * * The command runs with `sh -c` in `cwd`. It receives one JSON object on * stdin — `event`, `cwd`, `session_id`, `turn_id` inside a turn, and for * tool events `tool_name`, `tool_input` and (after the call) `tool_result`; * `subagent_stop` adds `parent_session_id` and `parent_turn_id` — and the * same facts as `NAMZU_HOOK_EVENT`, `NAMZU_SESSION_ID`, `NAMZU_TURN_ID`, * `NAMZU_TOOL_NAME` and, when the input names a file, `NAMZU_TOOL_PATH`. * * `session_start` and `session_end` run outside any turn, so they carry no * `turn_id` on stdin and no `NAMZU_TURN_ID` in the environment — not even * one inherited from the host's own environment. A hook that needs "which * turn" has it exactly when there is one. * * Its exit code is its answer: * * - `0` — carry on. * - `2` — **block.** Before a tool (`pre_tool_use`) the call is skipped and the * model is told why, with the hook's stderr as the reason. On any other * event a 2 is reported and the turn carries on: there is nothing left to * block once the tool has run or the turn has started. * - anything else — the hook's own failure. Reported through the logger, * never blocking. A formatter that crashed must not stop the agent editing, * and a script missing its interpreter (127) must not read as "forbidden". * * `2` rather than any non-zero, because a verdict and a failure are different * facts and one exit code cannot carry both. * * ## What a hook cannot do here * * Modify a tool's input or replace its result. The hook protocol allows both * (`modify`, `replace`); this adapter exposes neither, because a shell command * that rewrites what the model asked for deserves its own design and audit, * not a JSON field on stdout nobody reviewed. * * ## Bounds * * ## Renamed events * * `run_start`, `run_end` and `run_interrupt` became `turn_start`, `turn_end` * and `turn_interrupt`. A config that still names an old event is refused by * {@link attachShellHooks}, and the refusal names the new one: a hook that * silently stopped firing is worse than a config that fails to load. * * Each hook has a deadline (`timeoutMs`, default 30 s, capped at ten minutes) * and its output is captured to a bound. A hook that times out is reported * and does not block: one that could not answer in time has not answered no. */ import type { PluginId } from '../types/ids/index.js'; import type { PluginCompactionInfo, PluginHookDefinition, PluginHookEvent, PluginHookResult } from '../types/plugin/index.js'; import type { Logger } from '../utils/logger.js'; import type { PluginLifecycleManager } from './lifecycle.js'; /** * The events a shell hook may attach to: the ones an operator can act on * from a script. The model-call and iteration events stay in-process — * they carry the messages, and a shell is not where those go. */ export type ShellHookEvent = Extract; export declare const SHELL_HOOK_EVENTS: readonly ShellHookEvent[]; /** One shell command at one event. */ export interface ShellHookEntry { /** Run with `sh -c`, in the working directory, with the event JSON on stdin. */ readonly command: string; /** Tool names this applies to: `*`, a name, or `a|b|prefix*`. Absent means every tool. */ readonly matcher?: string; /** Deadline in milliseconds; default `DEFAULT_SHELL_HOOK_TIMEOUT_MS`, capped at `MAX_SHELL_HOOK_TIMEOUT_MS`. */ readonly timeoutMs?: number; } /** Event → entries, the shape a host's config file carries. */ export type ShellHooksConfig = { readonly [event in ShellHookEvent]?: readonly ShellHookEntry[]; }; /** The plugin id host shell hooks register under when the host names none. */ export declare const SHELL_HOOKS_PLUGIN_ID: PluginId; export declare const DEFAULT_SHELL_HOOK_TIMEOUT_MS = 30000; export declare const MAX_SHELL_HOOK_TIMEOUT_MS: number; /** * `matcher` against a tool name: `*`, or a `|`-separated list of names, each * of which may end in `*`. Case-sensitive, because tool names are. A matcher * on an event with no tool (`turn_start`) matches nothing unless it is `*`. */ export declare function shellHookMatches(matcher: string | undefined, toolName: string | undefined): boolean; /** What one hook run came back with, before it is mapped to a verdict. */ export interface ShellHookOutcome { readonly exitCode: number | null; readonly timedOut: boolean; readonly stdout: string; readonly stderr: string; /** Set when the process could not be started at all. */ readonly spawnError?: string; } export interface ShellHookInput { readonly event: ShellHookEvent; readonly cwd: string; /** Always present: every hook fires in a session. */ readonly sessionId: string; /** The turn the hook fired in. Absent on `session_start` and `session_end`. */ readonly turnId?: string; /** The delegating session, on `subagent_stop`. */ readonly parentSessionId?: string; /** The parent turn whose tool call spawned the child, on `subagent_stop`. */ readonly parentTurnId?: string; readonly prompt?: string; readonly compaction?: PluginCompactionInfo; readonly toolName?: string; readonly toolInput?: unknown; readonly toolResult?: { readonly success: boolean; readonly output: string; readonly error?: string; }; } /** Run one hook command to completion, bounded by its deadline and capture. */ export declare function runShellHook(entry: ShellHookEntry, input: ShellHookInput, signal?: AbortSignal): Promise; /** * The verdict, from the outcome and the event. Pure apart from the logger, * so the mapping — the part that decides what the model is told — is * testable without a process. */ export declare function shellHookVerdict(event: ShellHookEvent, entry: ShellHookEntry, outcome: ShellHookOutcome, log?: Logger): PluginHookResult; export interface ShellHookOptions { /** Where the command runs and what `cwd` says on stdin. */ readonly cwd: string; readonly log?: Logger; } /** One entry as a hook definition the lifecycle manager accepts. */ export declare function createShellHook(event: ShellHookEvent, entry: ShellHookEntry, options: ShellHookOptions): PluginHookDefinition; /** * Register every entry on the manager, in file order within an event. The * manager runs `post_*` hooks in reverse, as it does for plugins, so a * formatter registered last runs first after a write. Returns how many were * attached. * * Throws, before registering anything, when the config names a renamed event * (`run_start` and the rest); the message names the event that replaced it. */ export declare function attachShellHooks(manager: Pick, hooks: ShellHooksConfig, options: ShellHookOptions & { readonly pluginId?: PluginId; }): number; //# sourceMappingURL=shell-hook.d.ts.map