/** * The instruction files Claude Code loads from the directories ABOVE `dir`: * walking up to the filesystem root, every `ancestorChain` kind in each parent * is loaded IN FULL at launch, so a payload planted in a parent directory * reaches the model exactly like one in the project's own file. * * Returns CANDIDATES — absolute paths, existing or not, because this module * touches no filesystem. Most parents of any directory hold neither file, so a * caller that buckets its misses as "absent" should filter first: ~10 phantom * entries per session would drown the one signal that bucket carries, a target * that existed when the scan listed it and vanished before the read. * @param {string} dir the scan root; its own files are NOT included * @returns {string[]} */ export function ancestorInstructionFiles(dir: string): string[]; /** * Whether `file` lives inside `dir`. Lexical, on already-absolute paths, and the * bound on where an instruction-file scanner may REWRITE: a file above the scan * root is shared with every other project beneath that root, so it is reported * rather than silently edited. Both scanners ask this — one for a target it * globbed, one for a path an event handed it — and a copy each is a copy that * can drift into rewriting a file the other would not. * * Symlinks are deliberately not resolved: the guard that stops a link from * redirecting the write has to live at the write itself (cleanFile opens * O_NOFOLLOW), and resolving here would only duplicate it a check too early. * @param {string} dir * @param {string} file * @returns {boolean} */ export function isInsideDir(dir: string, file: string): boolean; /** * The one directory no instruction-file walk ever descends into. Its own * function so the name is spelled once, and so the two predicates that need it * (a plain glob walk, and {@link excludeFromContextScan}) cannot disagree. * * The LAST segment is what it reads: a dependency tree nested under a workspace * package is the same dependency tree, and an entry naming one arrives as the * path `packages/a/node_modules`, never as a bare name. * @param {string} entry a path relative to the scan root, `/`-separated * @returns {boolean} */ export function excludeNodeModules(entry: string): boolean; /** * Entries a context scan must not descend into or return: `node_modules`, and * every child of a `.claude` directory that is not whitelisted context. * * The globs alone would already refuse to MATCH those files, but a glob walker * calls this on directories as it walks and prunes the ones it rejects — which * is where the cost actually is. Without the prune, a `.claude/worktrees/` * holding a few repo checkouts is walked in full on every session start, and a * `.claude` NESTED inside a worktree is scanned as if it were this session's * context: a doubled-star segment does cross into a dot directory when the * pattern names one. * * Entries are paths relative to the scan root, so a top-level one is a bare * name: it carries no `.claude` context and is judged only against * `node_modules`. * @param {string} entry a path relative to the scan root, `/`-separated * @returns {boolean} */ export function excludeFromContextScan(entry: string): boolean; /** * Whether Claude Code announces loading `path` with an `InstructionsLoaded` * event — the `eventNamed` column of {@link CLAUDE_CONTEXT_KINDS}, asked of one * path. * * The complement of {@link contextScopeContradiction}, which checks the same * column against an event that DID fire. This one answers before any fires, so a * consumer can tell "the event is not coming" from "the event never came": a * launch carrying only an `AGENTS.md` or a skill has nothing for the host to * announce, and its silence is evidence of nothing. * * A path the table does not name gets `false`, the same conservative answer a * row added without the flag gets: this says an event IS coming, never that a * file is uninteresting. * @param {string} path absolute or relative; only its segments are read * @returns {boolean} */ export function announcedByInstructionsLoaded(path: string): boolean; /** * What a file the host just loaded as model context says about this table, or * null when it says nothing new. The InstructionsLoaded event is the only * observation that can prove the table wrong, and this is what it proves: * * - a `.claude/` subdirectory outside {@link CLAUDE_CONTEXT_SUBDIRS} loading * as context means the launch scan skips that whole directory — the file * here was scanned, every other file in it was not; * - a kind the table marks `eventNamed: false` being named means the event's * coverage is wider than the docs claim, and the lazy scan reaches files * nothing was crediting it with. * * Both observations are about what the host reaches ON ITS OWN, so both require * a host-chosen `loadReason`: an `@import` names a file the user's own markdown * pointed at, and acting on it would either whitelist an import target or credit * the event with a kind it reaches only when imported. An unrecognized reason is * treated the same way, so this loses a notice rather than inventing one. * * A path the table does not name at all says nothing about the table either, so * it returns null rather than guessing. A `claude-bulk` row is silent for the * reason in reverse: the table already knows that directory is storage. * @param {string} path the path the host loaded * @param {string} loadReason the event's `load_reason`, or "unknown" when the * host sent none; required rather than defaulted, since every observation here * holds only for a load the host chose itself * @returns {string | null} what is stale, phrased for whoever fixes the table */ export function contextScopeContradiction(path: string, loadReason: string): string | null; /** * Every kind of file an agent loads as model context — plus, as `claude-bulk` * rows, the `.claude/` directories that hold anything BUT context, so a consumer * filtering this table must filter on `shape` and never take it whole. * * Each row carries the two facts code branches on. `shape` says where the kind * lives; `ancestorChain` says whether Claude Code also loads it from the * directories ABOVE a scan root; `eventNamed` says whether `InstructionsLoaded` * names it as it loads, the claim {@link contextScopeContradiction} checks. * * Shapes: * - `dir-file` — `name`, in any directory (`packages/foo/CLAUDE.md`). * - `claude-md` — top-level markdown directly under a `.claude/` directory. * - `claude-subdir` — `.claude//` and everything markdown below it. * - `claude-bulk` — `.claude//`, holding data that is not context. * * The `claude-subdir` rows are a WHITELIST: an unlisted context directory costs * a scan nobody paid for anyway, while an unlisted BULK directory costs every * future session its startup. The `claude-bulk` rows name the bulk directories * this project has seen, so a load out of one asks for no whitelist entry. */ export const CLAUDE_CONTEXT_KINDS: readonly Readonly<{ shape: "dir-file" | "claude-md" | "claude-subdir" | "claude-bulk"; name: string; ancestorChain: boolean; eventNamed: boolean; }>[]; /** The `.claude/` subdirectories whose markdown loads as model context. */ export const CLAUDE_CONTEXT_SUBDIRS: readonly string[]; /** * Claude Code's own per-directory memory files: the kinds it loads from every * directory above a scan root as well as from the root itself. */ export const CLAUDE_MEMORY_FILES: readonly string[]; /** Every per-directory instruction file, memory files and `AGENTS.md` alike. */ export const CLAUDE_DIR_INSTRUCTION_FILES: readonly string[]; /** * Every glob whose matches an agent loads as model context ANYWHERE in a tree. * Claude Code loads these on entry to their containing directory — a load path * that bypasses the PostToolUse sanitizer — so a payload planted in e.g. * `packages/foo/CLAUDE.md` reaches the model uncleaned unless something scans it. * * This is the WHOLE-TREE scope, for a caller scanning a project on demand (the * CLI, the Python port). It is not what a SessionStart hook walks — see * {@link CLAUDE_LAUNCH_GLOBS} for why, and for what does. * * `**` does not descend into dot directories, so NESTED `.claude/` trees need * their own doubled-star-prefixed patterns: without them a directory-scoped * skill at `packages/foo/.claude/skills/x/SKILL.md` — model context by the same * load path — is never matched. That same rule is why the root `.claude` needs * no separate entry: a leading doubled star matches zero segments, so the * nested patterns cover the root tree too. * * Pair with {@link excludeFromContextScan}: the patterns alone already refuse to * MATCH a bulk directory, but only pruning the WALK avoids paying to read it. */ export const CLAUDE_INSTRUCTION_GLOBS: readonly string[]; /** * Every glob whose matches load AT LAUNCH from the scan root itself: the root's * own instruction files and its `.claude` context tree. * * Deliberately NOT recursive. A subdirectory's `CLAUDE.md` is loaded when Claude * Code reads a file in that subdirectory, not at launch, so globbing for it at * session start pays a whole-tree walk (the entire home directory, when the * session is launched there) to pre-scan files that mostly never load. The * InstructionsLoaded hook scans each of those at the moment it loads instead — * which is also the only moment that catches one created mid-session. * * Pair with {@link ancestorInstructionFiles} for the other half of the launch * set, and with {@link excludeFromContextScan} to prune the `.claude` walk. */ export const CLAUDE_LAUNCH_GLOBS: readonly string[]; /** * The `eventNamed` kinds spelled relative to the USER-GLOBAL config root — the * `~/.claude` (or `CLAUDE_CONFIG_DIR`) directory Claude Code loads at launch * whatever the project is. * * A second spelling because that root is a `.claude` directory ITSELF: its files * are `CLAUDE.md` and `rules/x.md`, not `.claude/rules/x.md`, and under * `CLAUDE_CONFIG_DIR` the path may carry no `.claude` segment at all — so * {@link announcedByInstructionsLoaded}, which reads a path's own segments, has * nothing there to classify by. Every row's shape is asserted in * test/claude-context.test.mjs, since a shape with no spelling here would glob * to nothing and read as "no event is coming". */ export const USER_GLOBAL_EVENT_NAMED_GLOBS: readonly string[];