Skip to main content

bash-classify

Classify bash commands by their side-effect risk level.

What it does

bash-classify parses bash expressions using tree-sitter, classifies each command against a database of 150+ known commands, and outputs a structured JSON verdict. Commands are classified along two axes: classification (READONLY, LOCAL_EFFECTS, EXTERNAL_EFFECTS, DANGEROUS, UNKNOWN) describing what kind of effects a command has, and risk (LOW, MEDIUM, HIGH) describing how worried you should be.

Designed primarily as a Claude Code hook to automatically allow low-risk commands while flagging risky ones for human review.

Installation

uv tool install bash-classify
# or
pip install bash-classify

Quick start

$ echo 'kubectl get pods -n production' | bash-classify | jq '.classification'
"READONLY"

$ echo 'git push --force origin main' | bash-classify | jq '.classification'
"DANGEROUS"

$ echo 'cp file.txt /etc/config' | bash-classify | jq '.classification'
"DANGEROUS"

$ echo 'find . -name "*.pyc" -delete' | bash-classify | jq '.classification'
"DANGEROUS"

Matching command shapes

Classification answers "how risky is this?". A deny hook usually has a narrower question: "does this expression run command shape X?" bash-classify match answers that one. It parses the expression, walks every invocation at every depth, and reports which of the shapes in a rules file were actually invoked — so a heredoc body, an echo string, a # comment or a grep pattern that merely names the command does not count.

# blocked-commands.yaml
rules:
  - name: mr-discussions-api
    command: [glab, api]
    any_arg_matches: 'merge_requests/[^/?]+/(discussions|notes)(/|\?|$)'

  - name: mr-view-comments
    command: [glab, mr, view]
    any_option: [--comments, -c]

  - name: mr-note
    command: [glab, mr, note]
    except: [[glab, mr, note, list]]
$ echo 'sudo glab mr note 42 -m hi' | bash-classify match --rules blocked-commands.yaml
{
  "matches": [
    {
      "rule": "mr-note",
      "command": ["glab", "mr", "note"],
      "argv": ["glab", "mr", "note", "42", "-m", "hi"],
      "via": ["sudo"]
    }
  ],
  "parse_warnings": []
}

Within one rule every condition given must hold; rules are independent of each other, and one invocation can match several. command is a prefix match against the resolved command path, so /usr/bin/glab --repo x mr note still resolves to glab mr note. any_option looks at the options actually present, with values stripped and declared clusters expanded (-wc carries -c). any_arg_matches is a Python re.search over every argument token — a pattern written for grep -E needs \S rather than [^[:space:]]. via lists the enclosing wrappers, outermost first.

Two things a caller has to check. First, parse_warnings is always present: when it is non-empty the expression could not be fully parsed, so an empty matches proves nothing and the caller should fall back to whatever it did before. Second, check that the output actually has a matches key. A bash-classify older than this mode does not reject the unknown match argument — it ignores it, classifies stdin and exits 0, so the caller gets a normal classification JSON with no matches key. Treat a non-zero exit, unparseable output, or output without a matches key as "cannot answer" and fall back; never read a missing matches as "nothing matched".

Exit codes are 0 whether or not anything matched, 1 for empty input or no input within 5 seconds, and 2 for bad arguments, an unreadable or invalid rules file, or an internal error.

Claude Code plugin

The repo includes a Claude Code plugin that auto-allows low-risk bash commands via a PreToolUse hook.

# Install the bash-classify CLI
uv tool install bash-classify

# Add the marketplace and install the plugin
claude plugin marketplace add fprochazka/bash-classify
claude plugin install bash-classify-hook@fprochazka-bash-classify

To upgrade after a new release:

uv tool install --force bash-classify
claude plugin marketplace update fprochazka-bash-classify
claude plugin update bash-classify-hook@fprochazka-bash-classify

Once installed, any Bash tool call with risk: LOW is auto-approved — no permission prompt. This includes all READONLY commands plus safe routine operations like git add, git commit, mkdir, package installs, code formatters, and more. Commands with MEDIUM or HIGH risk still require confirmation.

Command database

bash-classify loads command definitions from two locations:

  • Built-in database — 150+ command definitions bundled with the package, covering common Unix utilities, package managers, container tools, cloud CLIs, and more. Lives in src/bash_classify/commands/*.yaml.
  • User database — your own command definitions at ~/.config/bash-classify/commands/*.yaml (override the location with the BASH_CLASSIFY_CONFIG_DIR env var, which resolves to $BASH_CLASSIFY_CONFIG_DIR/commands/). User files with the same name as a built-in override it completely, so you can customize classifications for internal tools, company-specific wrappers, or personal CLIs without forking the repo.

Both directories use the same YAML format. See docs/classification-guidance.md for how to add new commands. YAML definitions are validated against a JSON Schema for IDE autocomplete and CI checks.

Classification levels

Level Description Examples
READONLY No side effects ls, cat, grep, kubectl get
LOCAL_EFFECTS Modifies local files or state only git add, git commit, cp, mkdir, pytest
EXTERNAL_EFFECTS Interacts with external systems git push, kubectl apply, curl -d
DANGEROUS Destructive, system-wide, or irreversible rm -rf, git push --force, chmod
UNKNOWN Command not in database Any unrecognized command

Risk levels

Each command also gets a risk rating, orthogonal to classification:

Risk Description Examples
LOW Safe, routine operation — auto-approved ls, git add, git commit, mkdir, ruff format
MEDIUM Normal caution warranted git push, cp, npm run, git rebase
HIGH Dangerous or unknown — always requires confirmation rm -rf, git push --force, unknown commands

Risk defaults are derived from classification (READONLY→LOW, LOCAL_EFFECTS→MEDIUM, EXTERNAL_EFFECTS→MEDIUM, DANGEROUS/UNKNOWN→HIGH) but can be overridden per command, subcommand, or option in the YAML database.

How it works

  • Tree-sitter parsing -- bash expressions are parsed into an AST for accurate command extraction, handling pipes, subshells, and command substitution
  • YAML command database -- each command has classification rules with subcommand and option matching
  • Subcommand matching -- kubectl get and kubectl delete can have different classifications
  • Multi-goal build tools -- subcommand_mode: match_all handles commands like mvn clean install and gradle clean build test where multiple goals can be combined in any order
  • Delegation for wrappers -- commands like xargs, sudo, and env delegate classification to the inner command
  • File path detection -- redirect operators (>, >>, <) are parsed into write_paths/read_paths in the output; writes to /tmp and /var/tmp stay at LOW risk

Python API

from bash_classify import classify_expression

result = classify_expression("kubectl get pods")
print(result.classification)  # Classification.READONLY
print(result.risk)            # Risk.LOW

Each command result carries the options it actually uses and the positionals left after parsing. Option values are stripped, so --key=value shows up as --key and -fvalue as -f:

command = classify_expression("git commit --amend -m 'wip'").commands[0]
print(command.command)      # ['git', 'commit']
print(command.options)      # ['--amend', '-m']
print(command.positionals)  # []

iter_invocations walks every invocation in an expression depth-first — top-level commands and, recursively, the inner commands that wrappers such as sudo, timeout or bash -c delegate to. It yields each invocation with its via chain: the enclosing wrappers, outermost first, empty at the top level.

from bash_classify import classify_expression, iter_invocations

for invocation, via in iter_invocations(classify_expression("sudo timeout 5 ls")):
    print(via, invocation.command)
# [] ['sudo']
# ['sudo'] ['timeout']
# ['sudo', 'timeout'] ['ls']

load_rules and match_expression are the same thing from Python:

from bash_classify import load_rules, match_expression

rules = load_rules("blocked-commands.yaml")
result = match_expression('cat > brief.md <<"EOF"\nmentions glab mr note\nEOF', rules)
print(result.matches)         # [] - the heredoc body is data, not a command
print(result.parse_warnings)  # []

See SPEC.md for the full specification.

Development

git clone https://github.com/fprochazka/bash-classify.git
cd bash-classify
uv sync --dev

Run tests and linting before committing:

uv run ruff format .
uv run ruff check .
uv run pytest

To add or modify command definitions, see docs/classification-guidance.md. All YAML files in src/bash_classify/commands/ are validated against a JSON Schema — your IDE will provide autocomplete if it supports the # $schema: comment.

Releasing

Version is derived automatically from git tags via hatch-vcs — no manual version bumping needed.

Before tagging, bump the version in both plugin manifest files:

  • coding-agent-plugins/claude-code/.claude-plugin/plugin.json
  • .claude-plugin/marketplace.json

Wait for CI to pass on master, then tag, push, and create a GitHub release:

# Review changes since last release
git log $(git describe --tags --abbrev=0)..HEAD --oneline

git tag v<version>
git push origin v<version>
gh release create v<version> --title "v<version>" --notes "..."

The publish.yml GitHub Action builds and publishes to PyPI automatically via trusted publishing.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bash_classify-0.10.0.tar.gz (161.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bash_classify-0.10.0-py3-none-any.whl (117.1 kB view details)

Uploaded Python 3

File details

Details for the file bash_classify-0.10.0.tar.gz.

File metadata

  • Download URL: bash_classify-0.10.0.tar.gz
  • Upload date:
  • Size: 161.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for bash_classify-0.10.0.tar.gz
Algorithm Hash digest
SHA256 cca8a2b15953ee74c192bf5de2c418b85d47d7f5629838490b58d0ef41f791c5
MD5 6e027f6abd619a9ac5902134b3bc9d0d
BLAKE2b-256 52f54828e40e31238a917c441d545b71a61f7931425a8e6e236e8b908d8d7a02

See more details on using hashes here.

Provenance

The following attestation bundles were made for bash_classify-0.10.0.tar.gz:

Publisher: publish.yml on fprochazka/bash-classify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bash_classify-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: bash_classify-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 117.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for bash_classify-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8b4c09d2ecc213a4d686ba58ab1a9351f5ab8f78aa06b8dc9862b56b7c57a2ef
MD5 a20d686c69bba7b96d6a66fa88914c66
BLAKE2b-256 cfb713b176866fd5b26e473aedaa3615e0b0991158f6817c869d4443cc05e2cd

See more details on using hashes here.

Provenance

The following attestation bundles were made for bash_classify-0.10.0-py3-none-any.whl:

Publisher: publish.yml on fprochazka/bash-classify

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.11.0

2 files

This release

0.10.0 This release

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page