/** * Scan every instruction file under the project, bucketing each unreadable * target through {@link classifyReadFailure}. `scanned` is DERIVED from the two * failure buckets, so the accounting invariant — every target is scanned, * skipped or absent — holds by construction and needs no comment restating it. * A target lost from that accounting is an instruction file that reaches the * model while the caller announces "clean". * @param {string} [dir] project root to scan (injectable for tests) * @returns {{ * targets: string[], * scanned: number, * findings: Array<{file: string, findings: ReturnType}>, * skipped: Array<{file: string, reason: string}>, * absent: string[], * }} */ export function scanProject(dir?: string): { targets: string[]; scanned: number; findings: Array<{ file: string; findings: ReturnType; }>; skipped: Array<{ file: string; reason: string; }>; absent: string[]; }; /** * The report for targets the scan could not read. Rendered into the alert the * PreToolUse gate surfaces, so an incomplete scan reaches the operator as a * checkpoint rather than as silence. * @param {Array<{file: string, reason: string}>} skipped * @returns {string} */ export function formatSkipped(skipped: Array<{ file: string; reason: string; }>): string; /** * The hook's CLI: scan the instruction files, auto-clean what it can, persist * the alert for the PreToolUse gate otherwise. Exported so a bundle entry * (which must claim the CLI slot before this module loads) can run the exact * same scan instead of duplicating it. * @param {{ * trace?: import("./lib/trace.mjs").TraceFn, * scan?: () => ReturnType, * sessionId?: string, * }} [opts] `trace` is where this scan announces engagement; a host with its * own trace channel passes its sink so the announcement lands where its * detector reads (see lib/trace.mjs). `scan` is the scanner, injectable so the * FAULT path below — a scanner that throws something other than an errno, i.e. * a bug — is drivable end to end; no filesystem state can force it, and an * untested fault path is how a posture goes missing in the first place. * `sessionId` keys the alert store this scan writes; the CLI entry reads it off * the SessionStart payload, and an in-process caller passes it directly. * @returns {Promise} */ export function cliMain(opts?: { trace?: import("./lib/trace.mjs").TraceFn; scan?: () => ReturnType; sessionId?: string; }): Promise; /** * The session identity from the SessionStart payload on stdin, or undefined when * the host sent none. * * Swallowing: the payload is read for ONE optional field, and a host that pipes * nothing (or malformed JSON) must still get the scan — a session-start scan * refused over an unparseable envelope is a strictly worse outcome than one * keyed to the shared `no-session` fallback. * @returns {Promise} */ export function sessionIdFromStdin(): Promise; /** * Read one file and run the SSOT scan over it. The scan logic itself (long-run * decode + scattered threshold-evasion counting) is `scanText`'s — a local * mirror used to re-count scatter from the raw STRIP match count, silently * re-growing the linguistic-joiner/VS15 false positive `scanText`'s carve-out * counter had already fixed. * @param {string} filePath * @returns {ReturnType} * `line` is 1-based, or `null` for the whole-file scattered-chars finding. */ export function scanFile(filePath: string): ReturnType; import { CLAUDE_CONTEXT_SUBDIRS } from "../src/claude-context.mjs"; import { CLAUDE_INSTRUCTION_GLOBS } from "../src/claude-context.mjs"; import { CLAUDE_LAUNCH_GLOBS } from "../src/claude-context.mjs"; /** * The SSOT decoder, re-exported through a lazy-bound wrapper (the binding is * `let` and may be re-bound by the cold-start reload, so the export must read * it at call time). A hand-written twin used to live here; it decoded tag * characters to RAW bytes — including actual C0 controls for U+E0001–U+E001F — * with no `untrusted data, not instructions:` framing or escaping, so the * hook's own report re-injected the hidden payload it had just caught. * @param {string} run * @returns {{ method: string, decoded: string }} */ export function decodeRun(run: string): { method: string; decoded: string; }; /** * Every file Claude Code loads as model context AT LAUNCH: `dir`'s own * instruction files and its `.claude/` context tree, plus the CLAUDE.md / * CLAUDE.local.md of every directory above it (loaded in full at launch, and * until now never scanned by anything). These load before the session's first * tool call — a path that bypasses the PostToolUse sanitizer — so a payload in * one of them reaches the model uncleaned unless it is scanned here. * * Bounded on purpose: one shallow glob plus a walk up the parent chain. The * `**`-rooted scope ({@link CLAUDE_INSTRUCTION_GLOBS}) walks the entire tree * below `dir`, which for a session launched in a home directory is ~100 seconds * of blocked startup spent on files Claude Code does not load at launch. Those * files load when a tool reads their directory, and scan-loaded-instructions * scans each one at that moment. * * lib/invisible-alert.mjs's `launchInstructionFiles` IS this function — shared * so its InstructionsLoaded gap-notice check reads the identical target set * this scan does, rather than a second enumeration that could drift. * @param {string} dir * @returns {string[]} */ export function findInstructionFiles(dir: string): string[]; import { alertAckFile } from "./lib/invisible-alert.mjs"; import { alertDir } from "./lib/invisible-alert.mjs"; export let LONG_RUN_RE: RegExp; export let LONG_RUN_THRESHOLD: 10; export let TOTAL_INVISIBLE_THRESHOLD: 30; export { CLAUDE_CONTEXT_SUBDIRS, CLAUDE_INSTRUCTION_GLOBS, CLAUDE_LAUNCH_GLOBS, alertAckFile, alertDir };