import { CommandScanner, CommandSegment } from '../command-scan'; /** * What ROLE one segment of a shell command plays — the question every allowlist-shaped bash guard has * to answer before it can judge a compound command. * * WHY this exists: `merged-branch-bash-guard` allowlists the commands that get you OFF a merged * branch, and the redirect it prints tells the agent to run them. An agent bounds tool output by * reflex, so it runs `git fetch origin main 2>&1 | tail -5` — and the guard, evaluating `tail -5` as * an ordinary command, denied the very remedy it had just printed. Verified pairs from the field: * * pnpm wp-cleanup allowed * pnpm wp-cleanup 2>&1 | tail -40 BLOCKED * git fetch origin main allowed * git fetch origin main 2>&1; echo BLOCKED * * Nothing in `| tail -40` or `; echo done` can touch the repo, and `done`/`do`/`for x in a b` are not * commands at all. So a segment is one of three things: * * - STRUCTURE — pure shell syntax, invokes nothing (`for b in a b c`, `done`, `fi`). * - SHAPING — cannot change the repo: a pager/filter fed by a PIPE (`| tail`, `| head`, `| wc`), * or an always-inert command (`echo`, `cd`, `pwd`, `true`). * - COMMAND — a real invocation, with `words` giving the effective argv AFTER leading shell * keywords are stripped, so `do gh pr list` classifies as `gh pr list`. * * Two things keep SHAPING honest. A filter counts only when a PIPE fed it — bare `tail src/x.ts` * reads the working tree and stays a COMMAND. And a segment carrying an output REDIRECT (`> file`, * `>> file`) is always a COMMAND, because `echo x > src/y.ts` writes the repo. `2>&1` is not a * redirect to a file and is deliberately not caught. * * Deciding WHETHER a shaping segment is acceptable is still the guard's call: merged-branch-bash-guard * pairs this with ContentReadScan so `git status | cat src/foo.ts` (a filter with a workspace path) * stays blocked. */ export type SegmentRole = 'structure' | 'shaping' | 'command'; /** Data-only (per CLAUDE.md, classes for data). */ export declare class SegmentVerdict { role: SegmentRole; /** The effective argv for a COMMAND, leading shell keywords stripped. Empty for the other roles. */ words: readonly string[]; constructor(role: SegmentRole, words: readonly string[]); } export declare class ShellSegmentScan { private readonly scanner; constructor(scanner?: CommandScanner); classify(segment: CommandSegment): SegmentVerdict; /** * The effective argv of a segment with leading shell keywords removed — what a guard should judge * instead of the raw words. `for b in $(…); do git status; done` splits into three segments and * the middle one is literally `do git status`; without this, `do` is the command name and every * loop body walks straight past a git allowlist. */ effectiveWords(segmentText: string): readonly string[]; private stripKeywords; /** * `>`, `>>`, `>out.txt`, `2>log` — but NOT `2>&1`/`1>&2`, which merely rewire fds. * * PUBLIC because RecoveryAllowlist asks the same question of a segment it is about to allow on the * strength of the program name alone (`curl`, `gh`): those cannot touch the tree by themselves, but * `curl … > src/x.ts` can. One implementation, so the two callers cannot disagree about what * counts as a redirect — the `2>&1` carve-out above is exactly the kind of detail a second copy * gets wrong. */ redirectsToFile(words: readonly string[]): boolean; }