/** * Decode a run of invisible characters to its likely payload. Recognizes the * two common smuggling encodings — Unicode tag characters (U+E0001–U+E007F map * directly to ASCII) and zero-width binary (ZWSP=0, ZWNJ=1, ZWJ=separator) — * and otherwise reports the raw code points. The tag-character payload is * rendered as a neutral, quoted/escaped `untrusted data, not instructions: "…"` * string (see {@link neutralizeTagBytes}) so the report can never re-inject the * hidden instruction, and only U+E0020–U+E007E map to raw printable ASCII. * @param {string} run * @returns {{ method: string, decoded: string }} */ export function decodeRun(run: string): { method: string; decoded: string; }; /** * Scan a file's text for hidden-Unicode injection. Reports each long invisible * run (with its decoded payload) plus a single scattered-chars finding when the * non-run invisible count crosses the threshold-evasion floor. * @param {string} content * @returns {Array<{ line: number | null, charCount: number, method: string, decoded: string }>} * `line` is the 1-based line of a long-run finding, or `null` for the * whole-file scattered-chars finding (not tied to a single line). */ export function scanText(content: string): Array<{ line: number | null; charCount: number; method: string; decoded: string; }>; /** * Every `globs` match under `cwd`, as an absolute path, with `node_modules` and * whatever `exclude` rejects pruned from the WALK rather than filtered from its * results — which is where a wide glob's cost actually is. * * `withFileTypes` is what makes the prune ANSWERABLE. Without it the walker * calls `exclude` once with an entry's bare name and again with its * root-relative path, so a predicate holding `build` cannot tell a top-level * `build/` from a tracked `src/build/` and prunes both; under an absolute * pattern it is handed absolute paths and matches neither. A Dirent carries an * absolute `parentPath`, which normalizes to exactly one root-relative, * `/`-separated entry per walked directory whatever shape the pattern has. * @param {string[]} globs * @param {string} cwd * @param {(entry: string) => boolean} [exclude] * @returns {string[]} absolute paths, one match per element */ export function walkContextGlobs(globs: string[], cwd: string, exclude?: (entry: string) => boolean): string[]; /** * Expand `globs` (relative to `cwd`) to absolute file paths, skipping * `node_modules`. The glob set is the caller's instruction-file convention. * * Containment is enforced per match (see {@link keepContained}): a match whose * glob pattern itself escapes `cwd` — via `..` or an absolute-path glob * outside the tree — THROWS, since reaching outside the tree is a caller * misconfiguration. A match that lexically stays inside `cwd` but resolves * (via an in-tree symlink) to a target outside the tree, or that simply * cannot be resolved (a dangling symlink or unreadable entry inside the * tree), is SKIPPED, so one bad symlink never aborts scanning the rest of the * project. * * `exclude` prunes the walk via {@link walkContextGlobs}, so it is handed one * root-relative, `/`-separated path per entry. It is composed with, never * replaces, the unconditional `node_modules` prune: a caller narrowing the scan * must not be able to widen it into a dependency tree. Pass * {@link excludeFromContextScan} to take Claude Code's own scope. * @param {string[]} globs * @param {{ cwd?: string, exclude?: (entry: string) => boolean }} [options] * @returns {string[]} */ export function findInstructionFiles(globs: string[], { cwd, exclude }?: { cwd?: string; exclude?: (entry: string) => boolean; }): string[]; /** * Scan every instruction file matched by `globs` and return only those with * findings, each path reported relative to `cwd`. Unreadable/missing files are * skipped. Pure scan — no mutation; pair with {@link cleanFile} to strip. * @param {string[]} globs * @param {{ cwd?: string, exclude?: (entry: string) => boolean }} [options] * `exclude` is forwarded to {@link findInstructionFiles} * @returns {Array<{ file: string, findings: ReturnType }>} */ export function scanInstructionFiles(globs: string[], { cwd, exclude }?: { cwd?: string; exclude?: (entry: string) => boolean; }): Array<{ file: string; findings: ReturnType; }>; /** * Atomically replace `absPath`'s contents with `data`, preserving `mode`. * * Writes to a sibling temp in the same directory, then `rename`s it over the * original (same dir => same filesystem => the rename is atomic, not a * cross-device copy). The temp name is UNPREDICTABLE (`tmpName()` defaults to * crypto-random) and the temp is created exclusively (O_CREAT|O_EXCL): if the * path already exists — including an attacker-planted symlink at a guessable * temp name — the open fails (EEXIST) and does NOT follow the link to clobber * its target. On the rare collision we fail loud rather than retry into a * different attacker-controlled path. * * Crash-safety (matching the doc claim): the temp fd is `fsync`ed before the * rename and the directory fd is `fsync`ed after it, so a power loss can't leave * the renamed name pointing at unflushed/empty data or lose the rename itself. * The EXACT `mode` is applied with `fchmod` (openSync's create mode is * umask-masked, so it alone would drop bits), and a failed write/sync `unlink`s * the temp before rethrowing so no partial temp leaks. `tmpName` and `remove` * are injectable fault-injection seams for tests (force a known temp path; drive * a cleanup-unlink failure); production callers never pass them. * @param {string} absPath * @param {string} data * @param {number} mode * @param {() => string} [tmpName] * @param {(path: string) => void} [remove] */ export function atomicReplaceFile(absPath: string, data: string, mode: number, tmpName?: () => string, remove?: (path: string) => void): void; /** * Strip payload-capable invisible characters from `absPath` in place. Returns * `true` when the file's bytes actually changed (a payload {@link scanText} * flags was removed) and `false` when {@link scanText} reports nothing to * strip. `true` means and only means "bytes changed", so a caller that flagged * this file and gets `false` back must NOT record it as cleaned — the file * changed under it, or the flagged run is one {@link stripInvisible} * preserves (a well-formed emoji-tag sequence), and either way the payload it * flagged is still there. There is no third return value: the `null` arm this * doc once described was dropped as dead (`stripInvisible` cannot leave the * bytes identical for anything `scanText` flags), and callers must branch on * the boolean rather than testing against `null`, which is vacuously true. * * Contract (scan/clean coherence): clean strips exactly what scan flags. A * write happens ONLY when `scanText` reports a finding, so the "scan, then * clean what scan flagged" workflow never silently rewrites a file scan called * clean. A handful of sub-threshold invisible chars (which scan ignores) are * left untouched — by design, the scanner's definition of a payload is the * single source of truth for what gets removed. * * Refuses to follow symlinks: instruction files must be regular files. The read * fd is opened with `O_NOFOLLOW`, so a symlinked path (which could redirect the * read/write to a target outside the tree) makes the OPEN itself fail — closing * the lstat→open TOCTOU window a separate stat would leave, in which the path * could be swapped to a symlink between the check and the read. * * Non-UTF-8 safety (O9): the file is read as raw BYTES and required to round-trip * losslessly through UTF-8 before any rewrite. `readFileSync(…, "utf-8")` * silently maps invalid bytes to U+FFFD, which a naive strip-and-rewrite would * then persist file-wide — so a non-UTF-8 file fails loud and is left untouched. * * Lost-update / TOCTOU guard: the on-path file is re-checked against the fstat * snapshot taken right after open (inode, size, mtime, and not-a-symlink) before * the rename; a concurrent write or symlink swap between our read and our write * fails loud rather than silently clobbering the other writer. * * The write is atomic (see {@link atomicReplaceFile}): stripped content goes to * a temp file in the same directory which is then `rename`d over the original * (preserving the original file mode), fsync'd for crash-safety. * * Throws if the file cannot be read or written (the caller decides whether an * unwritable contaminated file is fatal or falls back to alerting). * @param {string} absPath * @param {(path: string) => import("node:fs").Stats} [lstat] injectable * pre-rename recheck stat (fault-injection seam, mirrors * {@link atomicReplaceFile}'s `tmpName`): lets a test drive the concurrent * write/symlink-swap that the TOCTOU guard exists to catch, which is otherwise * unreachable from this fully-synchronous path. Defaults to `lstatSync`. * @returns {boolean} */ export function cleanFile(absPath: string, lstat?: (path: string) => import("node:fs").Stats): boolean; export { contextScanExclude } from "./repo-scope.mjs"; export { ancestorInstructionFiles, announcedByInstructionsLoaded, CLAUDE_CONTEXT_KINDS, CLAUDE_CONTEXT_SUBDIRS, CLAUDE_DIR_INSTRUCTION_FILES, CLAUDE_INSTRUCTION_GLOBS, CLAUDE_LAUNCH_GLOBS, CLAUDE_MEMORY_FILES, contextScopeContradiction, excludeFromContextScan, USER_GLOBAL_EVENT_NAMED_GLOBS } from "./claude-context.mjs";