The reference classifyCommand in Pattern 3.2 (and likely examples/guardrails/src/core/intent.ts) misclassifies several common commands. Each row below was produced by running the doc's snippet verbatim in Node (see repro script at the bottom):
| Command |
What the command actually does |
What the classifier thinks it's doing |
Result |
git log | grep fix |
Lists the commit log, then filters that text output for lines containing "fix". The grep reads from the pipe, not from any file. |
Reads git log (pass-through), then sees grep and thinks it's searching inside files for text. |
Classifies the grep segment as text_search and advises a file-search tool. Expected: ["pass_through"]. Actual: ["pass_through","text_search"]. |
awk '{print "a > b"}' f.txt |
Runs awk over f.txt and prints the literal string a > b for each line. The > is text inside the awk program, not a redirect. Nothing is written to a file. |
Sees a > and thinks awk is redirecting its output into a file (a destructive write). |
Classifies as file_modify and blocks the command. Expected: ["pass_through"]. Actual: ["file_modify"]. |
awk -f p.awk in 2>&1 |
Runs the awk program p.awk over in, and sends error messages to the same place as normal output (2>&1 merges stderr into stdout). No file is written. |
Sees > in 2>&1 and thinks awk is redirecting its output into a file (a destructive write). |
Classifies as file_modify and blocks the command. Expected: ["pass_through"]. Actual: ["file_modify"]. |
find . -exec rm {} \; |
Finds files and runs rm on each one. The \; is required syntax that marks the end of the -exec argument list — it is part of this one command. |
Sees a ; and thinks it's the boundary between two separate commands, so it cuts the command in half before classifying. |
The command is split at the \; and only the first half is classified. Expected: one find command. Actual: two fragments. |
1. command.split(/&&|\|\||;|\|/) is not quote- or escape-aware
Separators inside quotes, or an escaped \; (find's -exec terminator), are data, not command boundaries. find . -name "*.py" -exec rm {} \; gets split at the ;, and echo "a; b" would split mid-string. A small state machine that tracks single/double-quote state and backslash escapes fixes this.
2. /^\s*awk\b.*>\s*\S+/ over-matches
.*> greedily consumes a quoted > (inside an awk program) and stderr redirects like 2>&1, producing false file_modify blocks. The redirect check should only fire on an unquoted redirect operator that isn't a [0-9]>& fd-dup.
3. Splitting on | and classifying every segment is intent-blind
git log | grep fix flags the grep segment as text_search, but grep after a pipe is filtering output — it should pass. However, the naive fix (only classify the first command in a pipeline) opens a write-bypass: true | echo x > f.txt writes a file yet is never checked.
The correct rule is intent-dependent: skip post-pipe segments only for advisory intents (text_search / file_read / file_discovery); always check post-pipe segments for file_modify, since a redirect / sed -i / tee after a pipe still writes.
Repro script
const INTENT_PATTERNS = [
{ pattern: /^\s*sed\s+(-i|--in-place)\b/, type: 'file_modify' },
{ pattern: /^\s*awk\b.*>\s*\S+/, type: 'file_modify' },
{ pattern: /^\s*cat\s+\S+/, type: 'file_read' },
{ pattern: /^\s*(grep[rx]?|rg)\b/, type: 'text_search' },
{ pattern: /^\s*find\s+/, type: 'file_discovery' },
{ pattern: /^\s*fd\b/, type: 'file_discovery' },
{ pattern: /^\s*docker(-compose)?\b/, type: 'docker' },
];
function classifyCommand(command) {
const segments = command.split(/&&|\|\||;|\|/);
return segments.flatMap(segment => {
for (const { pattern, type } of INTENT_PATTERNS) {
if (pattern.test(segment.trim())) return [type];
}
return ['pass_through'];
});
}
Happy to send a PR with the state-machine splitter and an intent-aware pipe rule plus test cases if useful.
The reference
classifyCommandin Pattern 3.2 (and likelyexamples/guardrails/src/core/intent.ts) misclassifies several common commands. Each row below was produced by running the doc's snippet verbatim in Node (see repro script at the bottom):git log | grep fixgit log(pass-through), then seesgrepand thinks it's searching inside files for text.text_searchand advises a file-search tool. Expected:["pass_through"]. Actual:["pass_through","text_search"].awk '{print "a > b"}' f.txtf.txtand prints the literal stringa > bfor each line. The>is text inside the awk program, not a redirect. Nothing is written to a file.>and thinks awk is redirecting its output into a file (a destructive write).file_modifyand blocks the command. Expected:["pass_through"]. Actual:["file_modify"].awk -f p.awk in 2>&1p.awkoverin, and sends error messages to the same place as normal output (2>&1merges stderr into stdout). No file is written.>in2>&1and thinks awk is redirecting its output into a file (a destructive write).file_modifyand blocks the command. Expected:["pass_through"]. Actual:["file_modify"].find . -exec rm {} \;rmon each one. The\;is required syntax that marks the end of the-execargument list — it is part of this one command.;and thinks it's the boundary between two separate commands, so it cuts the command in half before classifying.\;and only the first half is classified. Expected: onefindcommand. Actual: two fragments.1.
command.split(/&&|\|\||;|\|/)is not quote- or escape-awareSeparators inside quotes, or an escaped
\;(find's-execterminator), are data, not command boundaries.find . -name "*.py" -exec rm {} \;gets split at the;, andecho "a; b"would split mid-string. A small state machine that tracks single/double-quote state and backslash escapes fixes this.2.
/^\s*awk\b.*>\s*\S+/over-matches.*>greedily consumes a quoted>(inside an awk program) and stderr redirects like2>&1, producing falsefile_modifyblocks. The redirect check should only fire on an unquoted redirect operator that isn't a[0-9]>&fd-dup.3. Splitting on
|and classifying every segment is intent-blindgit log | grep fixflags thegrepsegment astext_search, but grep after a pipe is filtering output — it should pass. However, the naive fix (only classify the first command in a pipeline) opens a write-bypass:true | echo x > f.txtwrites a file yet is never checked.The correct rule is intent-dependent: skip post-pipe segments only for advisory intents (
text_search/file_read/file_discovery); always check post-pipe segments forfile_modify, since a redirect /sed -i/teeafter a pipe still writes.Repro script
Happy to send a PR with the state-machine splitter and an intent-aware pipe rule plus test cases if useful.