bash-classify 0.5.0__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bash_classify-0.5.0 → bash_classify-0.6.0}/.claude-plugin/marketplace.json +1 -1
- {bash_classify-0.5.0 → bash_classify-0.6.0}/PKG-INFO +9 -5
- {bash_classify-0.5.0 → bash_classify-0.6.0}/README.md +8 -4
- {bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
- {bash_classify-0.5.0 → bash_classify-0.6.0}/schemas/command.schema.json +26 -0
- bash_classify-0.6.0/src/bash_classify/commands/gws.yaml +211 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/database.py +24 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/matcher.py +11 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/models.py +1 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_classifier.py +57 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_database.py +39 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/.claude/rules/command-database.md +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/.editorconfig +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/.github/workflows/ci.yml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/.github/workflows/publish.yml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/.gitignore +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/.python-version +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/CLAUDE.md +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/LICENSE +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/SPEC.md +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/docs/classification-guidance.md +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/pyproject.toml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/ruff.toml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/__init__.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/__main__.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/classifier.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/cli.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/apt.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/awk.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/basename.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/bash.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/black.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/brew.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/builtin.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cargo.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cat.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/chgrp.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/chmod.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/chown.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/column.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/comm.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/command.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cp.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/curl.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/cut.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/date.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/df.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/diff.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/dig.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/dirname.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/docker.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/du.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/echo.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/env.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/eslint.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/expand.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/false.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/file.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/find.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/fmt.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/fold.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/free.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gawk.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gcloud.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gh.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/git.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/glab.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/go.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gradle.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gradlew.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/grep.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/groups.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gunzip.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/gzip.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/hadolint.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/head.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/helm.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/hostname.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/id.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/install.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/isort.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/java.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/javac.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/join.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/journalctl.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/jq.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/kill.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/killall.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/kubectl.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/less.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ln.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ls.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/make.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/md5sum.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mkdir.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/more.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mv.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mvn.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mvnw.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/mypy.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/netstat.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nice.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nl.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/node.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nohup.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/npm.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/npx.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/nslookup.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/paste.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ping.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pip.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pip3.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pipx.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pkill.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/poetry.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/prettier.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/printenv.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/printf.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ps.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pup.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pwd.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/pytest.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/python.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/python3.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/read.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/readlink.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/realpath.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rev.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rg.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rm.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/rsync.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ruff.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/scp.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sed.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/seq.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sh.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sort.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ss.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/ssh.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/stat.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/strace.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/sudo.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/systemctl.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tac.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tail.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tar.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tee.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/terraform.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/test.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/time.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/timeout.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/top.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/touch.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tr.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/traceroute.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/true.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/tsc.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/type.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uname.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/unexpand.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uniq.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/unzip.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uptime.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/uv.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/wc.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/wget.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/whereis.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/which.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/whoami.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/xargs.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/xmllint.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/yarn.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/yes.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/yq.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/zip.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/commands/zsh.yaml +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/parser.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/src/bash_classify/py.typed +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/__init__.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/conftest.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_cli.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_cli_serialization.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_matcher.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_models.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_parser.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_real_world.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_schema.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/tests/test_smoke.py +0 -0
- {bash_classify-0.5.0 → bash_classify-0.6.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bash-classify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Classify bash commands as READONLY, WRITE, DANGEROUS, or UNKNOWN
|
|
5
5
|
Project-URL: Homepage, https://github.com/fprochazka/bash-classify
|
|
6
6
|
Project-URL: Repository, https://github.com/fprochazka/bash-classify
|
|
@@ -33,7 +33,7 @@ Classify bash commands by their side-effect risk level.
|
|
|
33
33
|
|
|
34
34
|
## What it does
|
|
35
35
|
|
|
36
|
-
bash-classify parses bash expressions using tree-sitter, classifies each command against a database of
|
|
36
|
+
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.
|
|
37
37
|
|
|
38
38
|
Designed primarily as a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) hook to automatically allow low-risk commands while flagging risky ones for human review.
|
|
39
39
|
|
|
@@ -86,10 +86,12 @@ Once installed, any Bash tool call with `risk: LOW` is auto-approved — no perm
|
|
|
86
86
|
|
|
87
87
|
## Command database
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
bash-classify loads command definitions from two locations:
|
|
90
90
|
|
|
91
|
-
-
|
|
92
|
-
-
|
|
91
|
+
- **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`.
|
|
92
|
+
- **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.
|
|
93
|
+
|
|
94
|
+
Both directories use the same YAML format. See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands. YAML definitions are validated against a [JSON Schema](schemas/command.schema.json) for IDE autocomplete and CI checks.
|
|
93
95
|
|
|
94
96
|
## Classification levels
|
|
95
97
|
|
|
@@ -118,7 +120,9 @@ Risk defaults are derived from classification (`READONLY`→LOW, `LOCAL_EFFECTS`
|
|
|
118
120
|
- **Tree-sitter parsing** -- bash expressions are parsed into an AST for accurate command extraction, handling pipes, subshells, and command substitution
|
|
119
121
|
- **YAML command database** -- each command has classification rules with subcommand and option matching
|
|
120
122
|
- **Subcommand matching** -- `kubectl get` and `kubectl delete` can have different classifications
|
|
123
|
+
- **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
|
|
121
124
|
- **Delegation for wrappers** -- commands like `xargs`, `sudo`, and `env` delegate classification to the inner command
|
|
125
|
+
- **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
|
|
122
126
|
|
|
123
127
|
## Python API
|
|
124
128
|
|
|
@@ -4,7 +4,7 @@ Classify bash commands by their side-effect risk level.
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
bash-classify parses bash expressions using tree-sitter, classifies each command against a database of
|
|
7
|
+
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.
|
|
8
8
|
|
|
9
9
|
Designed primarily as a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) hook to automatically allow low-risk commands while flagging risky ones for human review.
|
|
10
10
|
|
|
@@ -57,10 +57,12 @@ Once installed, any Bash tool call with `risk: LOW` is auto-approved — no perm
|
|
|
57
57
|
|
|
58
58
|
## Command database
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
bash-classify loads command definitions from two locations:
|
|
61
61
|
|
|
62
|
-
-
|
|
63
|
-
-
|
|
62
|
+
- **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`.
|
|
63
|
+
- **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.
|
|
64
|
+
|
|
65
|
+
Both directories use the same YAML format. See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands. YAML definitions are validated against a [JSON Schema](schemas/command.schema.json) for IDE autocomplete and CI checks.
|
|
64
66
|
|
|
65
67
|
## Classification levels
|
|
66
68
|
|
|
@@ -89,7 +91,9 @@ Risk defaults are derived from classification (`READONLY`→LOW, `LOCAL_EFFECTS`
|
|
|
89
91
|
- **Tree-sitter parsing** -- bash expressions are parsed into an AST for accurate command extraction, handling pipes, subshells, and command substitution
|
|
90
92
|
- **YAML command database** -- each command has classification rules with subcommand and option matching
|
|
91
93
|
- **Subcommand matching** -- `kubectl get` and `kubectl delete` can have different classifications
|
|
94
|
+
- **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
|
|
92
95
|
- **Delegation for wrappers** -- commands like `xargs`, `sudo`, and `env` delegate classification to the inner command
|
|
96
|
+
- **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
|
|
93
97
|
|
|
94
98
|
## Python API
|
|
95
99
|
|
|
@@ -6,6 +6,28 @@
|
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["command"],
|
|
8
8
|
"additionalProperties": false,
|
|
9
|
+
"oneOf": [
|
|
10
|
+
{
|
|
11
|
+
"description": "Alias file: points at another command, mutually exclusive with classification-bearing fields",
|
|
12
|
+
"required": ["alias_of"],
|
|
13
|
+
"not": {
|
|
14
|
+
"anyOf": [
|
|
15
|
+
{"required": ["classification"]},
|
|
16
|
+
{"required": ["risk"]},
|
|
17
|
+
{"required": ["subcommands"]},
|
|
18
|
+
{"required": ["options"]},
|
|
19
|
+
{"required": ["global_options"]},
|
|
20
|
+
{"required": ["subcommand_mode"]},
|
|
21
|
+
{"required": ["delegates_to"]},
|
|
22
|
+
{"required": ["strict"]}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"description": "Regular command definition",
|
|
28
|
+
"not": {"required": ["alias_of"]}
|
|
29
|
+
}
|
|
30
|
+
],
|
|
9
31
|
"properties": {
|
|
10
32
|
"command": {
|
|
11
33
|
"type": "string",
|
|
@@ -15,6 +37,10 @@
|
|
|
15
37
|
"type": "string",
|
|
16
38
|
"description": "Short description of the tool"
|
|
17
39
|
},
|
|
40
|
+
"alias_of": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Name of another command in the database whose definition should be used verbatim. Mutually exclusive with classification/risk/subcommands/options/global_options/subcommand_mode/delegates_to/strict."
|
|
43
|
+
},
|
|
18
44
|
"classification": {
|
|
19
45
|
"$ref": "#/$defs/Classification"
|
|
20
46
|
},
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# $schema: ../../../schemas/command.schema.json
|
|
2
|
+
command: gws
|
|
3
|
+
description: "Google Workspace CLI — manages Drive, Sheets, Gmail, Calendar, Docs, and more via Google APIs"
|
|
4
|
+
classification: READONLY
|
|
5
|
+
strict: false
|
|
6
|
+
|
|
7
|
+
# Dispatch model:
|
|
8
|
+
#
|
|
9
|
+
# `gws <service> <resource> [sub-resource] <method>` is a 3-4 level hierarchy.
|
|
10
|
+
# bash-classify only honors `subcommand_mode: match_all` on the top-level
|
|
11
|
+
# command, not on nested subcommands. To avoid enumerating every
|
|
12
|
+
# service × resource × method triple, we run the whole thing in match_all
|
|
13
|
+
# mode at the top. Every positional token is matched independently against
|
|
14
|
+
# the flat verb dictionary below; unknown tokens (service names like
|
|
15
|
+
# "drive", resource names like "files", IDs, etc.) fall back to the base
|
|
16
|
+
# READONLY classification. The final classification is the max severity
|
|
17
|
+
# across all matched tokens.
|
|
18
|
+
#
|
|
19
|
+
# This means:
|
|
20
|
+
# gws drive files list -> list (READONLY) -> READONLY
|
|
21
|
+
# gws drive files delete -> delete (DANGEROUS) -> DANGEROUS
|
|
22
|
+
# gws drive files create -> create (EXTERNAL_EFFECTS) -> EXTERNAL
|
|
23
|
+
# gws gmail users messages send -> send (EXTERNAL_EFFECTS) -> EXTERNAL
|
|
24
|
+
#
|
|
25
|
+
# The verb `export` is used both for "download as format" (drive/sheets/docs
|
|
26
|
+
# export = READONLY) and for `auth export`, which by default prints *masked*
|
|
27
|
+
# credentials (also READONLY). Only `auth export --unmasked` prints plaintext
|
|
28
|
+
# OAuth tokens and client secrets — that specific case is flagged via the
|
|
29
|
+
# `--unmasked` global option override below.
|
|
30
|
+
subcommand_mode: match_all
|
|
31
|
+
|
|
32
|
+
global_options:
|
|
33
|
+
--help: {overrides: READONLY}
|
|
34
|
+
-h: {overrides: READONLY}
|
|
35
|
+
--format: {takes_value: true}
|
|
36
|
+
--params: {takes_value: true}
|
|
37
|
+
--json: {takes_value: true}
|
|
38
|
+
--upload: {takes_value: true}
|
|
39
|
+
--upload-content-type: {takes_value: true}
|
|
40
|
+
--output: {takes_value: true}
|
|
41
|
+
--api-version: {takes_value: true}
|
|
42
|
+
--page-all: {}
|
|
43
|
+
--page-limit: {takes_value: true}
|
|
44
|
+
--page-delay: {takes_value: true}
|
|
45
|
+
--sanitize: {takes_value: true}
|
|
46
|
+
--dry-run: {overrides: READONLY, risk: LOW}
|
|
47
|
+
# `auth export --unmasked` prints plaintext OAuth tokens and client secrets.
|
|
48
|
+
--unmasked: {overrides: DANGEROUS, risk: HIGH}
|
|
49
|
+
|
|
50
|
+
subcommands:
|
|
51
|
+
# ── service names (explicit READONLY so they don't pollute risk) ──
|
|
52
|
+
drive: {classification: READONLY}
|
|
53
|
+
sheets: {classification: READONLY}
|
|
54
|
+
gmail: {classification: READONLY}
|
|
55
|
+
calendar: {classification: READONLY}
|
|
56
|
+
docs: {classification: READONLY}
|
|
57
|
+
slides: {classification: READONLY}
|
|
58
|
+
tasks: {classification: READONLY}
|
|
59
|
+
people: {classification: READONLY}
|
|
60
|
+
chat: {classification: READONLY}
|
|
61
|
+
classroom: {classification: READONLY}
|
|
62
|
+
forms: {classification: READONLY}
|
|
63
|
+
keep: {classification: READONLY}
|
|
64
|
+
meet: {classification: READONLY}
|
|
65
|
+
events: {classification: READONLY}
|
|
66
|
+
workflow: {classification: READONLY}
|
|
67
|
+
wf: {classification: READONLY}
|
|
68
|
+
script: {classification: READONLY}
|
|
69
|
+
admin: {classification: READONLY}
|
|
70
|
+
admin-reports: {classification: READONLY}
|
|
71
|
+
reports: {classification: READONLY}
|
|
72
|
+
modelarmor: {classification: READONLY}
|
|
73
|
+
schema: {classification: READONLY}
|
|
74
|
+
|
|
75
|
+
# ── auth: a flat "verb" since match_all treats every token equally ─
|
|
76
|
+
# `auth` itself maps to READONLY; the method tokens below elevate.
|
|
77
|
+
# `auth export` defaults to READONLY (masked); `--unmasked` is flagged
|
|
78
|
+
# separately via the global option override above.
|
|
79
|
+
auth: {classification: READONLY}
|
|
80
|
+
status: {classification: READONLY}
|
|
81
|
+
# `auth login` performs an OAuth2 handshake with Google — creates/rotates
|
|
82
|
+
# a credential server-side, so it has external effects.
|
|
83
|
+
login: {classification: EXTERNAL_EFFECTS}
|
|
84
|
+
logout:
|
|
85
|
+
classification: LOCAL_EFFECTS
|
|
86
|
+
risk: LOW
|
|
87
|
+
# `auth setup` can shell out to `gcloud` and create GCP OAuth clients/projects.
|
|
88
|
+
setup: {classification: EXTERNAL_EFFECTS}
|
|
89
|
+
|
|
90
|
+
# ── READONLY methods ──────────────────────────────────────────────
|
|
91
|
+
get: {classification: READONLY}
|
|
92
|
+
getProfile: {classification: READONLY}
|
|
93
|
+
getContent: {classification: READONLY}
|
|
94
|
+
getMetrics: {classification: READONLY}
|
|
95
|
+
getThumbnail: {classification: READONLY}
|
|
96
|
+
getByDataFilter: {classification: READONLY}
|
|
97
|
+
getBatchGet: {classification: READONLY}
|
|
98
|
+
batchGet: {classification: READONLY}
|
|
99
|
+
list: {classification: READONLY}
|
|
100
|
+
listLabels: {classification: READONLY}
|
|
101
|
+
listContactGroups: {classification: READONLY}
|
|
102
|
+
listDirectoryPeople: {classification: READONLY}
|
|
103
|
+
listScriptProcesses: {classification: READONLY}
|
|
104
|
+
listUserProcesses: {classification: READONLY}
|
|
105
|
+
searchContacts: {classification: READONLY}
|
|
106
|
+
searchDirectoryPeople: {classification: READONLY}
|
|
107
|
+
search: {classification: READONLY}
|
|
108
|
+
query: {classification: READONLY}
|
|
109
|
+
findDirectMessage: {classification: READONLY}
|
|
110
|
+
freebusy: {classification: READONLY}
|
|
111
|
+
instances: {classification: READONLY}
|
|
112
|
+
about: {classification: READONLY}
|
|
113
|
+
download: {classification: READONLY}
|
|
114
|
+
export: {classification: READONLY} # drive/sheets/docs export; NOTE: auth export also matches
|
|
115
|
+
generateIds: {classification: READONLY}
|
|
116
|
+
generateCseToken: {classification: READONLY}
|
|
117
|
+
|
|
118
|
+
# ── READONLY helpers ('+' prefix) ─────────────────────────────────
|
|
119
|
+
"+read": {classification: READONLY}
|
|
120
|
+
"+triage": {classification: READONLY}
|
|
121
|
+
"+agenda": {classification: READONLY}
|
|
122
|
+
"+standup-report": {classification: READONLY}
|
|
123
|
+
"+meeting-prep": {classification: READONLY}
|
|
124
|
+
"+weekly-digest": {classification: READONLY}
|
|
125
|
+
"+subscribe": {classification: READONLY}
|
|
126
|
+
|
|
127
|
+
# ── EXTERNAL_EFFECTS methods ──────────────────────────────────────
|
|
128
|
+
create: {classification: EXTERNAL_EFFECTS}
|
|
129
|
+
createContact: {classification: EXTERNAL_EFFECTS}
|
|
130
|
+
update: {classification: EXTERNAL_EFFECTS}
|
|
131
|
+
updateContent: {classification: EXTERNAL_EFFECTS}
|
|
132
|
+
updateContact: {classification: EXTERNAL_EFFECTS}
|
|
133
|
+
updateContactPhoto: {classification: EXTERNAL_EFFECTS}
|
|
134
|
+
patch: {classification: EXTERNAL_EFFECTS}
|
|
135
|
+
insert: {classification: EXTERNAL_EFFECTS}
|
|
136
|
+
import: {classification: EXTERNAL_EFFECTS}
|
|
137
|
+
send: {classification: EXTERNAL_EFFECTS}
|
|
138
|
+
copy: {classification: EXTERNAL_EFFECTS}
|
|
139
|
+
copyTo: {classification: EXTERNAL_EFFECTS}
|
|
140
|
+
copyOtherContactToMyContactsGroup: {classification: EXTERNAL_EFFECTS}
|
|
141
|
+
move: {classification: EXTERNAL_EFFECTS}
|
|
142
|
+
modify: {classification: EXTERNAL_EFFECTS}
|
|
143
|
+
modifyLabels: {classification: EXTERNAL_EFFECTS}
|
|
144
|
+
modifyAttachments: {classification: EXTERNAL_EFFECTS}
|
|
145
|
+
modifyAssignment: {classification: EXTERNAL_EFFECTS}
|
|
146
|
+
modifyIndividualStudentsOptions: {classification: EXTERNAL_EFFECTS}
|
|
147
|
+
batchUpdate: {classification: EXTERNAL_EFFECTS}
|
|
148
|
+
batchUpdateByDataFilter: {classification: EXTERNAL_EFFECTS}
|
|
149
|
+
batchModify: {classification: EXTERNAL_EFFECTS}
|
|
150
|
+
batchCreateContacts: {classification: EXTERNAL_EFFECTS}
|
|
151
|
+
batchUpdateContacts: {classification: EXTERNAL_EFFECTS}
|
|
152
|
+
append: {classification: EXTERNAL_EFFECTS}
|
|
153
|
+
upload: {classification: EXTERNAL_EFFECTS}
|
|
154
|
+
watch: {classification: EXTERNAL_EFFECTS}
|
|
155
|
+
stop: {classification: EXTERNAL_EFFECTS}
|
|
156
|
+
untrash: {classification: EXTERNAL_EFFECTS}
|
|
157
|
+
hide: {classification: EXTERNAL_EFFECTS}
|
|
158
|
+
unhide: {classification: EXTERNAL_EFFECTS}
|
|
159
|
+
enable: {classification: EXTERNAL_EFFECTS}
|
|
160
|
+
disable: {classification: EXTERNAL_EFFECTS}
|
|
161
|
+
verify: {classification: EXTERNAL_EFFECTS}
|
|
162
|
+
accept: {classification: EXTERNAL_EFFECTS}
|
|
163
|
+
reclaim: {classification: EXTERNAL_EFFECTS}
|
|
164
|
+
return: {classification: EXTERNAL_EFFECTS}
|
|
165
|
+
turnIn: {classification: EXTERNAL_EFFECTS}
|
|
166
|
+
quickAdd: {classification: EXTERNAL_EFFECTS}
|
|
167
|
+
setUp: {classification: EXTERNAL_EFFECTS}
|
|
168
|
+
setPublishSettings: {classification: EXTERNAL_EFFECTS}
|
|
169
|
+
completeImport: {classification: EXTERNAL_EFFECTS}
|
|
170
|
+
reactivate: {classification: EXTERNAL_EFFECTS}
|
|
171
|
+
publish: {classification: EXTERNAL_EFFECTS}
|
|
172
|
+
endActiveConference: {classification: EXTERNAL_EFFECTS}
|
|
173
|
+
# Model Armor sanitize* methods POST user-controlled text to the remote
|
|
174
|
+
# modelarmor.{region}.rep.googleapis.com endpoint.
|
|
175
|
+
sanitizeUserPrompt: {classification: EXTERNAL_EFFECTS}
|
|
176
|
+
sanitizeModelResponse: {classification: EXTERNAL_EFFECTS}
|
|
177
|
+
|
|
178
|
+
# ── EXTERNAL_EFFECTS helpers ──────────────────────────────────────
|
|
179
|
+
"+upload": {classification: EXTERNAL_EFFECTS}
|
|
180
|
+
"+push": {classification: EXTERNAL_EFFECTS}
|
|
181
|
+
"+send": {classification: EXTERNAL_EFFECTS}
|
|
182
|
+
# `gmail +watch` creates Pub/Sub topic+subscription and calls users.watch —
|
|
183
|
+
# leaves persistent server-side GCP resources unless --cleanup is passed.
|
|
184
|
+
"+watch": {classification: EXTERNAL_EFFECTS}
|
|
185
|
+
"+sanitize-prompt": {classification: EXTERNAL_EFFECTS}
|
|
186
|
+
"+sanitize-response": {classification: EXTERNAL_EFFECTS}
|
|
187
|
+
"+reply": {classification: EXTERNAL_EFFECTS}
|
|
188
|
+
"+reply-all": {classification: EXTERNAL_EFFECTS}
|
|
189
|
+
"+forward": {classification: EXTERNAL_EFFECTS}
|
|
190
|
+
"+insert": {classification: EXTERNAL_EFFECTS}
|
|
191
|
+
"+write": {classification: EXTERNAL_EFFECTS}
|
|
192
|
+
"+append": {classification: EXTERNAL_EFFECTS}
|
|
193
|
+
"+email-to-task": {classification: EXTERNAL_EFFECTS}
|
|
194
|
+
"+file-announce": {classification: EXTERNAL_EFFECTS}
|
|
195
|
+
"+renew": {classification: EXTERNAL_EFFECTS}
|
|
196
|
+
"+create-template": {classification: EXTERNAL_EFFECTS}
|
|
197
|
+
|
|
198
|
+
# ── DANGEROUS methods ─────────────────────────────────────────────
|
|
199
|
+
delete: {classification: DANGEROUS}
|
|
200
|
+
deleteContact: {classification: DANGEROUS}
|
|
201
|
+
deleteContactPhoto: {classification: DANGEROUS}
|
|
202
|
+
batchDelete: {classification: DANGEROUS}
|
|
203
|
+
batchDeleteContacts: {classification: DANGEROUS}
|
|
204
|
+
batchClear: {classification: DANGEROUS}
|
|
205
|
+
batchClearByDataFilter: {classification: DANGEROUS}
|
|
206
|
+
clear: {classification: DANGEROUS}
|
|
207
|
+
trash: {classification: DANGEROUS}
|
|
208
|
+
emptyTrash: {classification: DANGEROUS}
|
|
209
|
+
remove: {classification: DANGEROUS}
|
|
210
|
+
# Running an Apps Script project executes arbitrary code.
|
|
211
|
+
run: {classification: DANGEROUS}
|
|
@@ -167,8 +167,32 @@ def _yaml_str(value: object) -> str:
|
|
|
167
167
|
return str(value)
|
|
168
168
|
|
|
169
169
|
|
|
170
|
+
_ALIAS_FORBIDDEN_KEYS = frozenset(
|
|
171
|
+
{
|
|
172
|
+
"classification",
|
|
173
|
+
"risk",
|
|
174
|
+
"subcommands",
|
|
175
|
+
"options",
|
|
176
|
+
"global_options",
|
|
177
|
+
"subcommand_mode",
|
|
178
|
+
"delegates_to",
|
|
179
|
+
"strict",
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
170
184
|
def _parse_command_def(data: dict, command_name: str) -> CommandDef:
|
|
171
185
|
"""Parse a raw YAML dict into a CommandDef structure."""
|
|
186
|
+
alias_of = data.get("alias_of")
|
|
187
|
+
if alias_of is not None:
|
|
188
|
+
conflicting = sorted(_ALIAS_FORBIDDEN_KEYS & data.keys())
|
|
189
|
+
if conflicting:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"alias file for '{command_name}' has alias_of='{alias_of}' "
|
|
192
|
+
f"combined with forbidden field(s): {', '.join(conflicting)}"
|
|
193
|
+
)
|
|
194
|
+
return CommandDef(command=command_name, alias_of=str(alias_of))
|
|
195
|
+
|
|
172
196
|
classification = _parse_classification(data.get("classification"))
|
|
173
197
|
risk = _parse_risk(data.get("risk"))
|
|
174
198
|
|
|
@@ -94,6 +94,17 @@ def match_command(
|
|
|
94
94
|
classification_reason="command not in database",
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
+
# Step 1b: Resolve alias_of (max depth 1). The user-facing binary stays
|
|
98
|
+
# as typed; only command_def is swapped to the target.
|
|
99
|
+
if command_def.alias_of is not None:
|
|
100
|
+
target_name = command_def.alias_of
|
|
101
|
+
target_def = database.get(target_name)
|
|
102
|
+
if target_def is None:
|
|
103
|
+
raise ValueError(f"{binary} aliases '{target_name}' which is not in the database")
|
|
104
|
+
if target_def.alias_of is not None:
|
|
105
|
+
raise ValueError(f"alias chain too deep: {binary} -> {target_name} -> {target_def.alias_of}")
|
|
106
|
+
command_def = target_def
|
|
107
|
+
|
|
97
108
|
remaining = argv[1:]
|
|
98
109
|
|
|
99
110
|
# Step 2: Strip global options
|
|
@@ -135,6 +135,7 @@ class CommandDef:
|
|
|
135
135
|
"""Definition of a command (or subcommand) from the database."""
|
|
136
136
|
|
|
137
137
|
command: str
|
|
138
|
+
alias_of: str | None = None
|
|
138
139
|
classification: Classification | None = None
|
|
139
140
|
risk: Risk | None = None
|
|
140
141
|
global_options: dict[str, OptionDef] = field(default_factory=dict)
|
|
@@ -687,3 +687,60 @@ class TestFilePathDetection:
|
|
|
687
687
|
result = classify_expression("cat < /home/user/data.txt | tee /tmp/out.txt > /tmp/log.txt", database=database)
|
|
688
688
|
assert "/home/user/data.txt" in result.read_paths
|
|
689
689
|
assert "/tmp/log.txt" in result.write_paths
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
class TestAliasOfClassification:
|
|
693
|
+
"""Tests for the alias_of feature — an alias file points at another command def."""
|
|
694
|
+
|
|
695
|
+
def _make_db(self, tmp_path):
|
|
696
|
+
from bash_classify.database import load_database
|
|
697
|
+
|
|
698
|
+
(tmp_path / "target.yaml").write_text(
|
|
699
|
+
"command: target\n"
|
|
700
|
+
"classification: READONLY\n"
|
|
701
|
+
"strict: false\n"
|
|
702
|
+
"subcommands:\n"
|
|
703
|
+
" sub1: {classification: READONLY}\n"
|
|
704
|
+
" sub2: {classification: DANGEROUS}\n"
|
|
705
|
+
)
|
|
706
|
+
(tmp_path / "myalias.yaml").write_text("command: myalias\ndescription: pointer\nalias_of: target\n")
|
|
707
|
+
return load_database(tmp_path)
|
|
708
|
+
|
|
709
|
+
def test_alias_resolves_to_target(self, tmp_path) -> None:
|
|
710
|
+
db = self._make_db(tmp_path)
|
|
711
|
+
result = classify_expression("myalias sub1", database=db)
|
|
712
|
+
target_result = classify_expression("target sub1", database=db)
|
|
713
|
+
assert result.classification == target_result.classification
|
|
714
|
+
assert result.risk == target_result.risk
|
|
715
|
+
cmd = result.commands[0]
|
|
716
|
+
# matched_rule and command reflect the alias as typed
|
|
717
|
+
assert cmd.command[0] == "myalias"
|
|
718
|
+
assert cmd.matched_rule == "myalias.sub1"
|
|
719
|
+
|
|
720
|
+
def test_alias_resolves_dangerous_subcommand(self, tmp_path) -> None:
|
|
721
|
+
db = self._make_db(tmp_path)
|
|
722
|
+
result = classify_expression("myalias sub2", database=db)
|
|
723
|
+
assert result.classification == Classification.DANGEROUS
|
|
724
|
+
assert result.commands[0].matched_rule == "myalias.sub2"
|
|
725
|
+
|
|
726
|
+
def test_dangling_alias_raises(self, tmp_path) -> None:
|
|
727
|
+
import pytest
|
|
728
|
+
|
|
729
|
+
(tmp_path / "myalias.yaml").write_text("command: myalias\nalias_of: nonexistent\n")
|
|
730
|
+
from bash_classify.database import load_database
|
|
731
|
+
|
|
732
|
+
db = load_database(tmp_path)
|
|
733
|
+
with pytest.raises(ValueError, match="nonexistent"):
|
|
734
|
+
classify_expression("myalias foo", database=db)
|
|
735
|
+
|
|
736
|
+
def test_alias_chain_too_deep_raises(self, tmp_path) -> None:
|
|
737
|
+
import pytest
|
|
738
|
+
|
|
739
|
+
(tmp_path / "target.yaml").write_text("command: target\nclassification: READONLY\n")
|
|
740
|
+
(tmp_path / "mid.yaml").write_text("command: mid\nalias_of: target\n")
|
|
741
|
+
(tmp_path / "myalias.yaml").write_text("command: myalias\nalias_of: mid\n")
|
|
742
|
+
from bash_classify.database import load_database
|
|
743
|
+
|
|
744
|
+
db = load_database(tmp_path)
|
|
745
|
+
with pytest.raises(ValueError, match="alias chain too deep"):
|
|
746
|
+
classify_expression("myalias foo", database=db)
|
|
@@ -199,6 +199,45 @@ class TestStrictDefault:
|
|
|
199
199
|
assert grep.strict is False
|
|
200
200
|
|
|
201
201
|
|
|
202
|
+
class TestAliasOf:
|
|
203
|
+
def test_alias_file_parses(self, tmp_path: Path) -> None:
|
|
204
|
+
"""An alias file produces a CommandDef with alias_of set."""
|
|
205
|
+
target = tmp_path / "target.yaml"
|
|
206
|
+
target.write_text("command: target\nclassification: READONLY\n")
|
|
207
|
+
alias = tmp_path / "myalias.yaml"
|
|
208
|
+
alias.write_text("command: myalias\ndescription: pointer\nalias_of: target\n")
|
|
209
|
+
|
|
210
|
+
db = load_database(tmp_path)
|
|
211
|
+
alias_def = db["myalias"]
|
|
212
|
+
assert alias_def.alias_of == "target"
|
|
213
|
+
assert alias_def.command == "myalias"
|
|
214
|
+
assert alias_def.classification is None
|
|
215
|
+
assert alias_def.subcommands == {}
|
|
216
|
+
|
|
217
|
+
def test_alias_file_with_subcommands_raises(self, tmp_path: Path) -> None:
|
|
218
|
+
"""An alias file combined with classification-bearing fields is rejected."""
|
|
219
|
+
alias = tmp_path / "bad.yaml"
|
|
220
|
+
alias.write_text("command: bad\nalias_of: target\nsubcommands:\n sub: {classification: READONLY}\n")
|
|
221
|
+
db = load_database(tmp_path)
|
|
222
|
+
with pytest.raises(ValueError, match="subcommands"):
|
|
223
|
+
db["bad"]
|
|
224
|
+
|
|
225
|
+
def test_alias_file_with_classification_raises(self, tmp_path: Path) -> None:
|
|
226
|
+
alias = tmp_path / "bad.yaml"
|
|
227
|
+
alias.write_text("command: bad\nalias_of: target\nclassification: READONLY\n")
|
|
228
|
+
db = load_database(tmp_path)
|
|
229
|
+
with pytest.raises(ValueError, match="classification"):
|
|
230
|
+
db["bad"]
|
|
231
|
+
|
|
232
|
+
def test_alias_file_only_command_description_alias_of_is_valid(self, tmp_path: Path) -> None:
|
|
233
|
+
target = tmp_path / "target.yaml"
|
|
234
|
+
target.write_text("command: target\nclassification: READONLY\n")
|
|
235
|
+
alias = tmp_path / "myalias.yaml"
|
|
236
|
+
alias.write_text('command: myalias\ndescription: "some desc"\nalias_of: target\n')
|
|
237
|
+
db = load_database(tmp_path)
|
|
238
|
+
assert db["myalias"].alias_of == "target"
|
|
239
|
+
|
|
240
|
+
|
|
202
241
|
class TestUserCommandsDir:
|
|
203
242
|
def test_user_override_replaces_builtin(self, tmp_path: Path) -> None:
|
|
204
243
|
"""User YAML overrides built-in command definition."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh
RENAMED
|
File without changes
|
{bash_classify-0.5.0 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/hooks.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|