$ shellfirm
How It Works

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"
CommandRegex matches?Filter passes?Challenge shown?
git push --force origin mainYesYes (no --force-with-lease)Yes
git push --force-with-lease origin mainYesNo (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 commandAlternativeExplanation
rm -rf <path>trash <path>Moves to trash instead of permanent deletion
git push --forcegit push --force-with-leaseChecks that your local ref is up-to-date before force pushing
git resetgit stashSaves your changes to the stash so you can recover them later
git clean -fdgit clean -fdnDry-run mode shows what would be deleted without actually removing anything
git branch -Dgit branch -d <branch>Safe delete refuses to delete a branch with unmerged changes
git filter-branchgit-filter-repoFaster, safer, and officially recommended alternative
terraform apply -auto-approveterraform plan -out=plan.tfplan && terraform apply plan.tfplanReview the plan first, then apply from the saved plan file
terraform state mvterraform state mv -dry-runPreview the state change before modifying state
docker-compose down -vdocker-compose downWithout -v, volumes are preserved so data is not lost
docker system prune -adocker system pruneWithout -a, only dangling images are removed
aws ec2 terminate-instancesaws ec2 stop-instancesStop instead of terminate to preserve the instance
aws s3 rb s3://bucketaws s3 ls s3://bucketList bucket contents first to verify what would be deleted
redis-cli FLUSHALLredis-cli FLUSHDBFLUSHDB 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"
    }
  ]
}