import { type RunHookOptions } from "./run-hook.js"; /** A category of dangerous action a guard might be meant to block. */ export type DisasterCategory = "destructive-git" | "destructive-fs" | "bypass-verification" | "secret-exfiltration" | "remote-code"; /** One dangerous tool event a safety hook is expected to deny. */ export interface DisasterEvent { /** Stable id, e.g. "force-push". */ readonly id: string; /** One-line human label for the report. */ readonly label: string; /** What kind of guard should stop this. */ readonly category: DisasterCategory; /** The Claude Code tool the event names. */ readonly tool: string; /** The `tool_input` payload (what the hook inspects). */ readonly input: Record; } /** * The curated battery. Deliberately small and high-signal: each is a textbook * destructive action a real safety hook in the wild claims to stop. Extend with * care — every entry must be unambiguously dangerous so a miss is a real finding, * never noise (the don't-cry-wolf rule). */ export declare const DISASTER_CATALOG: readonly DisasterEvent[]; /** The result of feeding one disaster event to a hook. */ export interface GuardrailResult { readonly event: DisasterEvent; /** True iff the hook denied the call (exit 2 / decision:block / permissionDecision:deny). */ readonly blocked: boolean; /** The hook process exit code (1 ≠ block — the classic false-confidence bug). */ readonly exitCode: number; /** * Whether the hook was RUN for this event at all — false when its declared * condition ({@link VerifyGuardrailOptions.condition}) does not match, so the * harness would never spawn it here. * * 🔴 A NOT-RUN EVENT IS A MISS, and separating it out is the point. Before this * existed the battery could only ask "did it block?", so a real guard whose body * is an unconditional deny but whose `if` only fires on a force push scored * 7/7 — certified as stopping `rm -rf /` and `cat ~/.ssh/id_rsa`. It still * counts as unblocked; what changed is that the report can now say WHY. */ readonly ran: boolean; /** Why the hook ran or did not — the condition verdict, always present. */ readonly reason: string; } export interface VerifyGuardrailOptions extends RunHookOptions { /** Restrict the battery to these categories (default: the whole catalog). */ readonly categories?: readonly DisasterCategory[]; /** Override the catalog entirely (e.g. a project-specific battery). */ readonly events?: readonly DisasterEvent[]; /** The PreToolUse event name to wrap each disaster in (default "PreToolUse"). */ readonly event?: string; } /** * The same dangerous commands, spelled the other ways a shell reads identically. * * Takes a battery of hook test cases (shell commands wrapped as `PreToolUse` * events — `DISASTER_CATALOG` is the shipped one) and returns MORE test cases: * every command re-spelled with a quoted flag (`git push "--force"`), the short * form of a flag (`-f`), an absolute or escaped head (`/usr/bin/git`, `\git`), or a * pass-through wrapper (`sudo …`, `env …`). The shell runs each rewrite exactly as * it runs the original. Feed them to `assertBlocksDisasters` alongside the * originals: * * assertBlocksDisasters(hook, { * events: [...DISASTER_CATALOG, ...experimental_alternateSpellings(DISASTER_CATALOG)], * }); * * WHAT BREAKS WITHOUT IT. A guard whose rule is "the command contains `--force`" * blocks all seven catalog commands, so the battery is green — and lets * `git push "--force"` through, because the quotes make it a different string. * Measured 2026-09-02 on the shipped dogfood guard BEFORE this existed: 7/7 * originals blocked, **8 of 30** hand-written re-spellings blocked. * * WHY THE OUTPUT IS TRUSTWORTHY. Nothing new is judged. "Dangerous" is inherited * from the original a human put in the battery; "the same command" is decided by * the shell parser vigiles already uses for `runs()`/`touches()` (see * {@link sameOperation}). A rewrite that fails that check throws rather than being * emitted, so the battery can never quietly shrink. * * It returns ONLY the rewrites (never the originals), so pass the originals * alongside as above. Each rewrite keeps its original's `tool` and `category` * and takes the original's id with an index suffix (`force-push~4`), so a report * names which spelling got through. * * In promptfoo's vocabulary each rewrite rule here is a "strategy"; the difference * is that promptfoo's encodings (base64, leetspeak) may or may not be decoded by the * target, whereas every rewrite here is one the shell provably executes identically * — a miss is a guard bug, never an ambiguous input. * * @experimental Days old with a single consumer (this repo's own dogfood) and no * external use. The set of rewrite rules and the id-suffix shape are the parts most * likely to move; the prefix says so at every call site, which an import line or a * doc note cannot. */ export declare function experimental_alternateSpellings(events: readonly DisasterEvent[]): readonly DisasterEvent[]; /** * Run a hook command against the disaster battery and report which events it blocks. * `hookCommand` is the exact shell the hook registers (e.g. `bash hooks/guard.sh` or * `npx vigiles hook-runtime guard`); it receives each disaster as a PreToolUse event on stdin, * exactly as Claude Code would deliver it. */ export declare function verifyGuardrail(hookCommand: string, opts?: VerifyGuardrailOptions): GuardrailResult[]; /** Did the hook miss any event it was given? (the false-confidence signal). */ export declare function unblockedDisasters(results: readonly GuardrailResult[]): GuardrailResult[]; /** * Assert a hook blocks every disaster in the (selected) battery — the CI gate. * Throws with the misses listed, so a guardrail that's secretly a no-op fails the * build instead of failing in production. */ export declare function assertBlocksDisasters(hookCommand: string, opts?: VerifyGuardrailOptions): void; /** * One battery event as a report line, WITHOUT leading indentation so each caller * nests it where its own layout needs. * * THREE outcomes, not two. "never run" is not a weaker "allows": the harness * would not invoke this hook for that call at all, so the guard has no opinion * to report. Printing it as `allows` is what made a conditional guard look like * it had considered — and permitted — commands it can never see. * * @internal Shared by {@link formatGuardrailReport} and the directory-level * sweep's formatter (`experimental_formatPluginGuardReport`), so the two renderers * cannot drift into two vocabularies for the same three outcomes. Not part of the * public API — a caller wanting these lines wants one of the two reports. */ export declare function guardrailRow(result: GuardrailResult): string; /** * Render a coverage report (informational, NEUTRAL). It reports what the * hook blocks WITHOUT judging it: a hook that allows these may simply not be a * bash-safety guard (our own pre-edit.sh blocks .md edits, not `rm -rf`). The * "false confidence" verdict only applies once intent is DECLARED — see * {@link assertBlocksDisasters}. */ export declare function formatGuardrailReport(hookCommand: string, results: readonly GuardrailResult[]): string; //# sourceMappingURL=guardrail-check.d.ts.map