import { CommandScanner, CommandSegment } from '../command-scan'; /** * Decides the one question stale-main-bash-guard asks of a command: does this segment put stale * WORKSPACE FILE CONTENT into the agent's context? * * The distinction that matters is content vs metadata, not shell vs tool. `git log`, `git diff`, * `git status`, builds, tests and the cure itself are all fine on a stale main — none of them hands * you the text of a file that upstream has moved past. `cat src/x.ts` does, and so does * `grep -r foo services/`, `ls .github/workflows/` (the incident's actual wrong answer: a listing * missing a workflow that existed upstream) and `git show HEAD:file`. * * Three things keep this from over-blocking: * 1. A piped consumer reads stdin, not the tree — `git log | grep fix` is metadata, so it passes. * 2. A reader with no path operand that does not default to the cwd reads stdin — `cat` alone, * `grep pattern` alone. * 3. Only paths INSIDE the workspace count. `cat /etc/hosts`, `cat ~/.zshrc`, `cat /tmp/out.log` * are nothing to do with this repo's staleness. */ export declare class ContentReadScan { private readonly scanner; private readonly workspaceRoot; private readonly shell; private readonly baseDir; /** * `effectiveCwd` is the directory the command really runs in — after its own leading `cd`, which * is how an agent reaches a linked worktree, since the harness resets a cwd that left the workspace. * RELATIVE operands are resolved against it, not against workspaceRoot: `cd /tmp/scratch && cat * notes.md` reads `/tmp/scratch/notes.md`, which is nothing to do with this repo's staleness, * while `cd /tmp/scratch && cat /repo/src/x.ts` still names repo content and is still caught. * Defaults to workspaceRoot, which is exactly the old behaviour (relative = inside the repo). */ constructor(scanner: CommandScanner, workspaceRoot: string, effectiveCwd?: string); /** * True when this segment's ONLY job is reading content, and nothing it reads is in the workspace — * `ls -la ~/.claude/projects/`, `cat /tmp/out.log`, `grep -r x /other/repo`. * * merged-branch-bash-guard needs this: it default-denies bash on a merged branch, and denied an * `ls` of a directory outside every git repo on the grounds that the branch was merged. Nothing * about a read that never touches the tree is affected by which branch the tree is on. The * "content reader" restriction is what keeps this from becoming a general escape hatch: a build, * a server or a git write is not a content reader and never qualifies, however its paths look. */ readsOnlyOutsideContent(segment: CommandSegment): boolean; private isContentReader; /** * The command word that reads stale workspace content, or null when this segment does not. * The returned string is only a log/diagnostic label. * * Judged on the segment's EFFECTIVE words: `for f in a b; do cat $f; done` splits into segments * whose middle one is literally `do cat $f`, and taking `do` as the command name let every loop * body read the stale tree unseen. */ readsStaleContent(segment: CommandSegment): string | null; /** * git's own content readers. `git grep` searches tracked CONTENT and `git show :` * prints a file — both stale when the rev is local. Against an `origin/…` rev they read the * CURRENT upstream tree, which is exactly what we want the agent doing, so those pass. */ private gitContentRead; private pathOperands; /** * Is this operand a path inside the workspace? A RELATIVE operand is resolved against the * directory the command actually runs in (`baseDir`), so it counts only when that directory is * itself in the tree — the old code assumed every relative path meant "inside the repo", which is * how a command run in a `/private/tmp` scratchpad got judged as reading a stale repo. An * absolute path counts only when it is genuinely under workspaceRoot, so `/etc/hosts`, * `~/notes.md` and `/tmp/x` are not this repo's problem. * * Deliberately NOT filesystem-checked: whether the path exists says nothing about staleness, and * a stat per operand on the blocking hook path is exactly the cost these guards avoid. */ private isWorkspacePath; private isInWorkspace; private isEscapeHatchPath; private baseName; }