True iff the command is provably side-effecting (bash-effects classifier).
True iff the command pipes into a BARE shell interpreter (curl … | sh,
… | bash -s) — the remote-code-execution shape. High-signal: a shell leaf
WITH a script-file argument (sh deploy.sh) is NOT flagged; only a shell
reading from stdin is, which only happens downstream of a pipe.
True iff a leaf command runs program (e.g. "git push"), optionally with --force/-f.
Optionalopts: { force?: boolean }True iff a leaf command MENTIONS a path under one of the prefixes (e.g.
~/.ssh, .env) — the secret-read / sensitive-path matcher. Sees the path
however the command is wrapped (cd x && cat ~/.ssh/id_rsa).
MENTIONS, not writes. grep -c x notes/S.md touches notes — so does
rm -rf notes. Pairing this with isSideEffecting, which classifies
the WHOLE command line, does NOT recover the difference: a plain read whose
line happens to be side-effecting for an unrelated reason (grep -c x notes/S.md 2>/dev/null) matches both and gets blocked. To gate WRITES to a
directory, use writesTo; conflating the two is the trap.
OVER-INCLUSIVE ON PURPOSE, AND THE OPPOSITE WAY ROUND FROM PathView.under. This is a DENYLIST primitive — every caller spells
touches(secret) ? deny() : allow() — so a miss is an ALLOW, not silence. An
unprovable answer is therefore reported as a MATCH: a token that could name
something under the prefix under some root or some cd matches, and the gate
blocks. See prefixVerdict for the three answers and matchesPrefix for the two biases.
True iff a leaf command CREATES OR MODIFIES a file under one of the prefixes — the "don't let Bash write here" matcher, and the precise counterpart to touches. Two sources, both AST-backed:
cmd > f, cmd >> f, cmd >| f, cmd &> f.
This is the single most common write shape, and it lives on the statement,
not in any argv.sed -i, cp/mv/install (destination), tee, dd of=,
truncate, shred.A path merely READ never matches — cat a/paper.md, grep x a/paper.md,
cp a/paper.md /tmp/x (the source is read, /tmp/x is the write) are all
false for writesTo(["a"]). Quoting is handled by the parser, so a write
QUOTED inside another command does not match: in
echo 'echo y > a/paper.md' > /tmp/note.txt the only real target is
/tmp/note.txt.
Deletion is a different question and is deliberately NOT reported here —
pair with runs("rm") if a gate cares about removal too.
Exactly writeTargets(prefixes).length > 0, and implemented as that — reach
for writeTargets when the gate needs to know WHICH file.
The write targets of this command that fall under one of the prefixes — the "WHICH file is written" counterpart to writesTo. Same two AST-backed sources (redirection targets + file-writing programs' argv positions), same denylist bias (an undecidable placement is INCLUDED).
Spelling is as-written after normalization — quote-unwrapped,
$HOME-canonicalized, and resolved against the leaf's own chdir wrapper
(env -C dir sed -i x reports dir/x) — in order of appearance, exact
duplicates collapsed. Filter by basename/suffix; never re-match the prefixes
by hand, because hand-rolled prefix matching is the exact source of the
trailing-slash, absolute-path and root-blindness defects this vocabulary
exists to remove.
An empty array ⇔ writesTo(prefixes) === false, so the natural
writeTargets(P).some(pred) needs no emptiness check to behave correctly.
prefixes is REQUIRED and there is no unfiltered overload: the raw list
does not cross the API boundary, because a consumer holding it has to
re-implement the matching that prefixVerdict exists to own.
An AST-backed view of a Bash command — the author never writes a regex.