Skip to content

Pattern 3.2 classifyCommand has correctness bugs (quote/escape-blind split, greedy redirect regex, intent-blind pipe handling) #3

Description

@dgrant

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions