safety: close four classifier coverage gaps - #4081
Merged
Merged
Conversation
…d compose down -v Destructive-pattern matching is contiguous: 'docker system prune --volumes' (HIGH) does not match 'docker system prune -f --volumes' because -f sits between the two anchor tokens, so the command falls back to bare 'docker system prune' (MEDIUM) and under-reports the blast radius when the --volumes flag is actually present. Same story on the compose side: 'docker compose down --volumes' (HIGH) does not cover 'docker compose down --remove-orphans -v' or 'docker compose down -v --remove-orphans', which land on bare 'docker compose down' (LOW-MEDIUM) even though named volumes are wiped. Add explicit variants for every observed ordering of -f, -a, -af, -fa, -v, --volumes, and --remove-orphans, plus regression tests.
The destructive taxonomy is Unix-only; Windows users running the shell tool in PowerShell can execute destructive commands (Remove-Item, Clear-Content) that the classifier reports as unknown, so the UI cannot label them and permissive safety modes lose the chance to warn on blast radius. Add PowerShell equivalents for the fs-delete and fs-overwrite categories, covering: - Remove-Item ... -Recurse -Force / -Force -Recurse (HIGH, rm -rf equivalent) - Remove-Item ... -Recurse (HIGH, still irreversible on non-tty) - Remove-Item ... -Force (MEDIUM, suppresses confirmation) - Remove-Item <path> (LOW, prompts on tty) - Clear-Content (MEDIUM, in-place truncate) Positional, -Path, and trailing-path forms are enumerated so the observed argument orderings all match. Argument matching is already case-insensitive via normalizeCommand, so 'remove-item' and 'REMOVE-ITEM' also match.
Destructive intent frequently arrives via a wrapper — 'docker exec <ctr> …', 'kubectl exec …', an SSH login — that hides the dangerous verb inside the inner command. The current taxonomy only inspects the outer shell, so real data-loss commands come back as unknown and permissive safety modes can't tell the UI what the blast radius actually is. Add pattern groups that fire on the destructive verb wherever it appears in the command line, so wrapping through docker exec / kubectl exec / ssh inherits the classification automatically: - SQL DDL/DML (drop database, drop table, drop schema, truncate table, delete from) — matches across mysql, mariadb, psql, sqlite3, and the -e or -c one-shot forms these clients accept. - App-framework reset verbs (n8n db:reset and user-management:reset, Frappe bench new-site --force / drop-site, Rails db:drop / db:reset / db:schema:load, Django manage.py flush, Prisma migrate reset, Sequelize db:drop) — one-shot commands that wipe or replace application state. - Key-value flushes (redis-cli flushall / flushdb). Regression tests cover the docker-exec-wrapped forms as well as the bare client invocations.
trungutt
marked this pull request as ready for review
August 31, 2026 13:17
…oundaries The destructive-pattern regex prefix is (?:^|.*\b), which fires on any word boundary in the normalised command. That's the right anchor for alphanumeric verbs like "rm" or "docker", but wrong for patterns whose leading token is itself a shell metacharacter — ">", "<", "|", "&", ";". A word boundary sits between any word char and the metachar, so: - "--email <EMAIL> --firstName Admin" matches the truncate pattern "> <file>" via the l> boundary inside the redacted placeholder. - 'git commit -m "feat: 1>0 check"' matches the same pattern via 1>0. - Any placeholder or in-string comparison that happens to sit between two word characters spuriously escalates to destructive/fs-overwrite. Anchor shell-metacharacter patterns on \s instead: a real redirect, pipe, or chain operator is preceded by whitespace (or the start of the line) after normalizeCommand collapses runs to single spaces. Verbal patterns keep the \b anchor so 'cd /tmp && rm -rf foo' still resolves its embedded 'rm -rf' correctly.
trungutt
force-pushed
the
safety-classifier-coverage-gaps
branch
from
August 31, 2026 13:22
5dcde06 to
05937b1
Compare
Sayt-0
requested changes
Aug 31, 2026
Sayt-0
left a comment
Member
There was a problem hiding this comment.
Solid direction: each gap is real, the commits are atomic, and the whitespace-anchor fix removes a confirmed false positive. Two behavior changes need attention before merge, and roughly half of the new pattern entries are already covered by existing ones.
Verified behavior deltas (master vs this branch):
| command | master | this PR | restricted mode impact |
|---|---|---|---|
grep "drop table users" -r . |
safe | destructive / high | ALLOW to DENY |
git log --grep "delete from cart" |
safe | destructive / medium | ALLOW to DENY |
rg "DELETE FROM users" |
safe | destructive / medium | ALLOW to DENY |
run 1> /tmp/out.log |
destructive / medium | unknown | none (ask either way) |
docker exec ... --email <EMAIL> ... |
destructive / medium (FP) | unknown | none |
Blocking:
- The unanchored SQL patterns flip safe-listed read-only searches to destructive (table above). Since destructive matching wins over the safe list, any
grep/rg/git log --grepwhose argument contains SQL text now prompts underbalancedand is denied underrestricted. This trade-off may be acceptable (precedent:grep "rm foo"already matchesrm <single-file>), but it is a silent behavior change for a common agent workflow and deserves an explicit decision, a note in the taxonomy, and regression tests either way.
Non-blocking but worth fixing in the same pass:
- 6 of the 9 new docker entries are already covered by existing patterns (details inline); the pattern language's
...wildcard can replace the enumeration entirely. Remove-Item "C:\Program Files\..." -Recurse -Forceclassifies LOW because<path>compiles to\S+and cannot span a quoted path with spaces (details inline).redis-cli -h host FLUSHALLandbench new-site <site> --forcefall back to unknown (details inline).
Notes:
- Two claims in the PR description do not reproduce on master:
git commit -m "feat: 1>0 check"never matched (the truncate pattern requires a space after>), anddocker compose down -v --remove-orphanswas already HIGH viadocker compose down -v(the compiled suffix(?:$|\b.*)accepts trailing content). Worth correcting so the taxonomy history stays accurate. - The
\sanchor drops fd-number redirects (1> file,2> file) from the truncate pattern. Gating is unchanged (destructive and unknown gate identically), and the2> /dev/nullfalse positive disappears, so this looks like an acceptable trade; a sentence in the commit message or pattern notes would make it deliberate.
…w SQL patterns Addresses the review on docker#4081: - Docker: the classifier's compiled patterns already have a trailing '\b.*' suffix, so 'docker system prune --volumes' already covers '--volumes -f/-a/-af/-fa' on master; enumerating those orderings was redundant. Drop them and add wildcard entries 'docker system prune ...--volumes', 'docker compose down ...-v', and 'docker compose down ...--volumes' to close the only genuine gaps ('-f --volumes', '-fa --volumes', and '--remove-orphans -v/--volumes'). - PowerShell: '<path>' compiles to '\S+' and cannot span a quoted path with spaces ('C:\Program Files\…', 'OneDrive - Corp\…'), so the enumerated -Recurse/-Force orderings fell through to LOW on every Windows path that contains a space. Collapse the 14 Remove- Item entries to two wildcard forms plus a positional base case. - Redis / Frappe: connection flags on 'redis-cli' ('-h', '-p', '-n') and the site-first ordering on 'bench new-site … --force' broke contiguity and reported unknown. Wildcards fix both. - SQL: 'drop database' / 'drop table' / 'delete from' matched inside the quoted arguments of safe-listed search commands, flipping 'grep "drop table users" -r .' from safe to destructive/high. Under 'restricted' this changes ALLOW to DENY on a common workflow. Anchor every SQL DDL/DML pattern on a known client executable (mysql / mariadb / psql / sqlite3); the leading '.*\b' anchor keeps wrapper commands (docker exec, kubectl exec, ssh) matching. Add regression tests both for the safe cases and for the accepted precedence caveat (destructive wins when the client name itself appears in the search argument).
melmennaoui
approved these changes
Aug 31, 2026
Sayt-0
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The shell-safety classifier reports
unknown— losing the blast-radius label and category tag it would otherwise expose — for a set of commands that are unambiguously destructive, and under-reports the blast radius on twodockercommands where a critical flag is present but appears in a non-canonical position. Permissive safety modes end up asking for confirmation without saying why, and (for the flag-order cases) miss that the user is asking to delete named volumes.Four independent gaps, one commit each:
1. Flag-order variants for
docker system prune --volumesanddocker compose down -vDestructive patterns are matched contiguously.
docker system prune --volumes(HIGH) does not coverdocker system prune -f --volumesbecause-fsits between the two anchor tokens, so the command falls back to baredocker system prune(MEDIUM) even though the--volumesflag is present and named volumes will be wiped.The same shape recurs on compose:
docker compose down --volumes(HIGH) does not coverdocker compose down --remove-orphans -vordocker compose down -v --remove-orphans.Fix: enumerate the observed flag orderings for
-f/-a/-af/-fa/-v/--volumes/--remove-orphans.2. PowerShell destructive patterns
The taxonomy is Unix-only. Windows users running the shell tool in PowerShell produce destructive commands (
Remove-Item -Path <path> -Recurse -Force,Clear-Content) that classify asunknown. Add fs-delete and fs-overwrite entries for the PowerShell equivalents, covering positional/-Path/trailing-path forms and both-Recurse -Forceand-Force -Recurseorderings.3. Destructive SQL, app-CLI, and key-value patterns
Destructive intent frequently arrives via a wrapper —
docker exec <ctr> …,kubectl exec …, an SSH login — that hides the dangerous verb inside the inner command. Because the existing taxonomy only inspects the outer shell verb, real data-loss commands (DROP DATABASE,TRUNCATE TABLE,redis-cli FLUSHALL,n8n db:reset,rails db:reset,bench new-site --force, …) come back asunknown.Add pattern groups that fire on the destructive verb wherever it appears, so wrapping through
docker exec/kubectl exec/sshinherits the classification automatically:drop database,drop schema,drop table,truncate table,delete from(coversmysql -e,psql -c,sqlite3 db "…",mariadb -e).n8n db:reset,n8n user-management:reset,bench new-site --force,bench drop-site,rails db:drop/db:reset/db:schema:load,manage.py flush,prisma migrate reset,sequelize db:drop.redis-cli flushall,redis-cli flushdb.4. Whitespace anchor for shell-metachar patterns
The destructive-pattern regex prefix is
(?:^|.*\b), which fires on any word boundary. That's the right anchor for alphanumeric verbs (rm,docker), but wrong for patterns whose leading token is itself a shell metacharacter —>,<,|,&,;. A word boundary sits between any word char and the metachar, so:--email <EMAIL> --firstName Adminmatched the truncate pattern> <file>via thel>boundary inside a redacted placeholder. This is the only case that reproduced on master; the placeholder shape (word-char,>, space) is what defeats the\banchor.Anchor shell-metacharacter patterns on
\sinstead: a real redirect, pipe, or chain operator is preceded by whitespace (or start of line) afternormalizeCommandcollapses runs to single spaces. Verbal patterns keep the\banchor.Notes for the reviewer
Each commit is standalone and lands one gap. Regression tests are colocated in
safety_test.goin each commit.