Filters & Alternatives
How post-match filters reduce false positives and alternatives suggest safer commands
shellfirm uses two mechanisms to improve the quality of interceptions: filters that reduce false positives, and alternatives that suggest safer commands.
Filters
Filters are evaluated after a regex pattern matches. All filters in a check must pass (logical AND) for the check to fire. If any filter fails, the match is silently discarded.
NotContains
The most common filter type. It suppresses a match when the command contains a specific substring. This is how shellfirm distinguishes between dangerous and safe variants of similar commands.
Example: git push --force vs --force-with-lease
The git:force_push check matches git push --force. But git push --force-with-lease is a safer alternative that should not trigger the check. The NotContains filter handles this:
- id: git:force_push
test: git\s{1,}push\s{1,}.*(-f\b|--force)
severity: High
filters:
- type: NotContains
value: "--force-with-lease"
| Command | Regex matches? | Filter passes? | Challenge shown? |
|---|---|---|---|
git push --force origin main | Yes | Yes (no --force-with-lease) | Yes |
git push --force-with-lease origin main | Yes | No (contains --force-with-lease) | No |
Example: git clean with dry-run
- id: git:clean_force
test: git\s{1,}clean\s{1,}-fd
severity: High
filters:
- type: NotContains
value: "--dry-run"
- type: NotContains
value: "-n"
Both git clean -fd --dry-run and git clean -fdn are safe (dry-run mode), so both NotContains filters are needed.
Example: kubectl delete with dry-run
- id: kubernetes:delete_namespace
test: (kubectl|k)\s+delete\s+(ns|namespace)
severity: Critical
filters:
- type: NotContains
value: "--dry-run"
Example: aws s3 rm with dryrun
- id: aws:s3_recursive_delete
test: aws\s+s3\s+rm\s+s3://.*--recursive
severity: High
filters:
- type: NotContains
value: "--dryrun"
Contains
The inverse of NotContains. The match only fires if the command contains a specific substring. This is useful for patterns that should only trigger in specific situations.
PathExists
Suppresses the match when the target file or directory does not exist on disk. The value is the regex capture-group index (1-based) that contains the path.
Example: rm -rf with path validation
- id: fs:recursively_delete
test: 'rm\s{1,}(?:-rf|-Rf|...)?\s*(\*|\.{1,}|/)\s*$'
severity: Critical
filters:
- type: PathExists
value: 1
The PathExists: 1 filter checks whether the path captured by regex group 1 actually exists. If you type rm -rf /nonexistent/path, the check does not fire because the path does not exist.
This prevents false positives for typos and non-existent paths, while still protecting actual files.
Alternatives
When a check matches, shellfirm can suggest a safer command as an alternative. Alternatives are displayed in the challenge prompt and included in audit logs and MCP risk assessments.
Each check can specify two fields:
- alternative -- The safer command to use instead
- alternative_info -- An explanation of why the alternative is safer
Examples
| Dangerous command | Alternative | Explanation |
|---|---|---|
rm -rf <path> | trash <path> | Moves to trash instead of permanent deletion |
git push --force | git push --force-with-lease | Checks that your local ref is up-to-date before force pushing |
git reset | git stash | Saves your changes to the stash so you can recover them later |
git clean -fd | git clean -fdn | Dry-run mode shows what would be deleted without actually removing anything |
git branch -D | git branch -d <branch> | Safe delete refuses to delete a branch with unmerged changes |
git filter-branch | git-filter-repo | Faster, safer, and officially recommended alternative |
terraform apply -auto-approve | terraform plan -out=plan.tfplan && terraform apply plan.tfplan | Review the plan first, then apply from the saved plan file |
terraform state mv | terraform state mv -dry-run | Preview the state change before modifying state |
docker-compose down -v | docker-compose down | Without -v, volumes are preserved so data is not lost |
docker system prune -a | docker system prune | Without -a, only dangling images are removed |
aws ec2 terminate-instances | aws ec2 stop-instances | Stop instead of terminate to preserve the instance |
aws s3 rb s3://bucket | aws s3 ls s3://bucket | List bucket contents first to verify what would be deleted |
redis-cli FLUSHALL | redis-cli FLUSHDB | FLUSHDB only clears the current database, not all databases |
How alternatives appear
In the terminal challenge prompt:
============ RISKY COMMAND DETECTED ============
Severity: HIGH
Description: This command will force push and overwrite remote history.
Alternative: git push --force-with-lease
(Checks that your local ref is up-to-date before force pushing,
preventing accidental overwrites of others' work.)
? Type `yes` to continue Esc to cancel ›
In MCP risk assessments (JSON):
{
"alternatives": [
{
"command": "git push --force-with-lease",
"explanation": "Checks that your local ref is up-to-date...",
"source": "regex-pattern"
}
]
}