/** * The path prefix every alert artifact of ONE session under this project shares. * * Session-keying is what makes the gate's one-time ask correct by construction. * Keying by PROJECT alone, reset by a destructive clear at SessionStart, would * leave two ways for a session to inherit the previous one's answer: an * early-exiting scanner arm (a dep-load failure) returns before the clear, and * nothing pins SessionStart against the InstructionsLoaded events fired for * the files loaded at launch. A session that cannot see another session's * files needs neither the clear nor the ordering — past sessions' artifacts * simply age out through {@link sweepStaleSessions}. * @param {string} [sessionId] the harness's session identity * @returns {string} */ export function sessionPrefix(sessionId?: string): string; /** * The directory holding this session's alert findings, one file per finding. * @param {string} [sessionId] * @returns {string} */ export function alertDir(sessionId?: string): string; /** * Companion marker the PreToolUse gate writes once it has surfaced the alert * this session, so the gate asks ONCE then degrades to a passive reminder * instead of prompting on every tool call. Session-keyed like the findings it * answers for, so a fresh session cannot read an older session's answer. * @param {string} [sessionId] * @returns {string} */ export function alertAckFile(sessionId?: string): string; /** * Marker the InstructionsLoaded scanner writes on every fire, so another hook * can tell whether that event is being scanned at all this session. * @param {string} [sessionId] * @returns {string} */ export function instructionsLoadedFile(sessionId?: string): string; /** * Companion marker: the notice below has been surfaced this session. * @param {string} [sessionId] * @returns {string} */ export function instructionsLoadedNoticeFile(sessionId?: string): string; /** * Whether the InstructionsLoaded scanner has run this session — i.e. whether the * lazily-loaded instruction files are being scanned at all. Ownership-validated * like every other marker here: a co-tenant could otherwise plant the * predictable path and suppress the notice below, which is the whole signal that * nested files are going unscanned. * @param {string} [sessionId] * @returns {boolean} */ export function instructionsLoadedSeen(sessionId?: string): boolean; /** * Delete this project's artifacts from OTHER sessions once they are older than * the TTL. The current session's own prefix is skipped, so the sweep can never * answer its own question wrong; every other session's files are past history * that nothing reads. * * This replaces the destructive SessionStart clear: with the store session-keyed * there is nothing to reset, only old state to age out. * @param {string} [sessionId] the session whose artifacts must be kept * @returns {void} */ export function sweepStaleSessions(sessionId?: string): void; /** * Record that the InstructionsLoaded scanner engaged. Symlink-safe presence * write (see writeSentinelFile) at a predictable $TMPDIR path. * * The event fires once per instruction file loaded, so the already-recorded case * returns without a write — and the stale-session sweep rides the FIRST fire of * a session, where one readdir is paid once rather than per loaded file. * @param {string} [sessionId] * @returns {void} */ export function recordInstructionsLoaded(sessionId?: string): void; /** * Companion marker: this session already found the LAUNCH set empty — that set * (which files load at session start) cannot change mid-session, so a second * glob of `dir` can never change that half of the answer. It says nothing about * a directory touched later; {@link instructionsLoadedGapNotice}'s `touchedDir` * covers that half fresh on every call instead. * @param {string} [sessionId] * @returns {string} */ export function launchEmptyFile(sessionId?: string): string; /** * Every file Claude Code loads as model context AT LAUNCH from `dir`: its own * instruction files, its `.claude/` context tree, and the CLAUDE.md / * CLAUDE.local.md of every directory above it. THE SSOT scan-invisible-chars.mjs * reads too (re-exported there as `findInstructionFiles`) — sharing the one * function is what keeps the SessionStart scan's targets and this module's * launch-emptiness check from drifting into two different answers for "what * loads at launch". See src/claude-context.mjs for why this is the shallow * launch scope and not a whole-tree walk. * @param {string} dir * @returns {string[]} */ export function launchInstructionFiles(dir: string): string[]; /** * The one-time context line for a session where no InstructionsLoaded scan ran, * or null when the scan has been seen, the notice already ran this session, the * host's cold-start marker says setup is still running, or neither launch nor * `touchedDir` could have fired the event. * * PURE for the notice: nothing is recorded until the caller confirms it landed * in a returned response. Launch-emptiness is cached once found — fixed for the * session — but `touchedDir` is re-checked every call: the claim below is about * SUBDIRECTORY files, so an empty launch must not silence a later call whose * tool touched a directory that DOES carry real, unscanned content. * * SessionStart scans what loads at launch; a subdirectory's file is scanned by * the event; no scan means nothing says so — unless nothing touched so far held * a file the event would have ANNOUNCED, with bytes in it. The three remaining * causes are named together, since * the marker cannot tell them apart: an unwired event, a Claude Code older than * EVENT_MIN_CLI_VERSION, or the hook disabled via AGENT_SANITIZER_DISABLED_HOOKS. * @param {string} [sessionId] the harness's session identity (see * instructionsLoadedFile) * @param {string} [dir] the project root to check (injectable; defaults to * the real project) * @param {string} [touchedDir] this tool call's own target directory, when * known — re-checked every call, never cached * @returns {string | null} */ export function instructionsLoadedGapNotice(sessionId?: string, dir?: string, touchedDir?: string): string | null; /** * Record that the gap notice above was surfaced, so it rides on ONE tool call * rather than every one — the per-call repeat is what trains a reader to skip it. * Called only once the notice is in a response that is actually being returned. * @param {string} [sessionId] * @returns {void} */ export function recordInstructionsLoadedNotice(sessionId?: string): void; /** * The alert findings if invisible-char injection was detected in instruction * files and couldn't be auto-cleaned, else null. * * The store is a DIRECTORY of one file per finding, all at predictable, * world-visible $TMPDIR paths, so both the directory and every entry in it are * attacker-plantable: trust the directory only when it is a real directory this * uid owns (a symlink would let a co-tenant aim this reader at unrelated files), * each entry only when markerIsTrusted confirms a regular file this uid owns, * then scrub the bytes through Layer-1 before any caller splices them into a * reason — the report would otherwise carry ANSI/invisible spoofing into the * model's context. * This session's store AND the shared `no-session` fallback, because a hook that * faults BEFORE it can parse its payload has no session identity to key by: its * finding lands in the fallback, and a strictly session-keyed read would leave * the one report of an unscanned instruction file unreachable. The ack stays * strictly session-keyed, so the gate still asks exactly once per session. A * fallback finding is read only while it is inside FALLBACK_TTL_MS, which is * what keeps it from re-arming the gate for a later session. * @param {string} [sessionId] * @returns {string | null} */ export function invisibleCharAlert(sessionId?: string): string | null; /** * Add `text` to the alert the PreToolUse gate surfaces this session, keeping * whatever is already there. * * One O_EXCL-created, randomly-named file per finding. A single file appended * through a read-modify-write would let two hooks recording a finding at once * silently drop one of them; a fresh file per finding has no shared cell to * lose. Symlink-refusing (writeFileNoFollow) because the store sits at a * predictable, world-visible $TMPDIR path. * @param {string} text * @param {string} [sessionId] * @returns {boolean} whether the finding was recorded */ export function appendAlert(text: string, sessionId?: string): boolean; /** * True once the gate has surfaced its blocking ask this session. Validates * ownership (not mere existence): a co-tenant could pre-create the ack at its * predictable $TMPDIR path to permanently suppress the one-time blocking ask down * to the passive reminder, so trust the marker only when it is a regular file * this uid wrote (markerIsTrusted), mirroring how acknowledgeAlert writes it. * * On a host that exports no session id every session shares the fallback prefix, * so the ack has no session to end with and would suppress the one-time blocking * ask down to the passive reminder for the life of the machine. There it expires * with {@link FALLBACK_TTL_MS} like the findings it answers for. * @param {string} [sessionId] * @returns {boolean} */ export function alertAcknowledged(sessionId?: string): boolean; /** * Record that the gate has surfaced its blocking ask, so later tool calls get a * passive reminder instead of an ask on every call. Session-keyed, so the next * session re-asks once without anything having to clear this. * @param {string} [sessionId] * @returns {void} */ export function acknowledgeAlert(sessionId?: string): void; /** * The blocking ask. The heading states only that the scan did not finish clean: * the alert carries injection findings, unreadable targets, or a scanner fault, * and each report names its own kind. A heading that asserted "injection * detected" mislabelled the other two. * @param {string} findings * @returns {string} */ export function gateAskReason(findings: string): string; /** * Non-blocking reminder for tool calls after the first ask: the injection is * still present, but the user was already asked once this session, so this rides * as context rather than re-prompting on every call. * @returns {string} */ export function gateReminderContext(): string; /** * The path prefix every alert artifact of this PROJECT shares. Never a file * itself — only {@link sessionPrefix} and the sweep read it — so that one * `startsWith` covers every artifact the sweep must age out. */ export const ALERT_BASE: string;