bash-classify 0.4.2__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.4.2 → bash_classify-0.6.0}/.claude-plugin/marketplace.json +1 -1
- {bash_classify-0.4.2 → bash_classify-0.6.0}/PKG-INFO +9 -5
- {bash_classify-0.4.2 → bash_classify-0.6.0}/README.md +8 -4
- {bash_classify-0.4.2 → bash_classify-0.6.0}/SPEC.md +19 -1
- {bash_classify-0.4.2 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
- {bash_classify-0.4.2 → bash_classify-0.6.0}/docs/classification-guidance.md +9 -1
- {bash_classify-0.4.2 → bash_classify-0.6.0}/schemas/command.schema.json +26 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/classifier.py +47 -2
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/cli.py +10 -0
- bash_classify-0.6.0/src/bash_classify/commands/gws.yaml +211 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tee.yaml +1 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/database.py +24 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/matcher.py +11 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/models.py +5 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_classifier.py +113 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_cli_serialization.py +99 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_database.py +39 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/.claude/rules/command-database.md +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/.editorconfig +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/.github/workflows/ci.yml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/.github/workflows/publish.yml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/.gitignore +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/.python-version +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/CLAUDE.md +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/LICENSE +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/pyproject.toml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/ruff.toml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/__init__.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/__main__.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/apt.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/awk.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/basename.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/bash.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/black.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/brew.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/builtin.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cargo.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cat.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/chgrp.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/chmod.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/chown.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/column.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/comm.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/command.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cp.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/curl.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cut.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/date.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/df.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/diff.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/dig.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/dirname.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/docker.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/du.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/echo.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/env.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/eslint.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/expand.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/false.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/file.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/find.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/fmt.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/fold.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/free.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gawk.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gcloud.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gh.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/git.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/glab.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/go.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gradle.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gradlew.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/grep.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/groups.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gunzip.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gzip.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/hadolint.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/head.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/helm.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/hostname.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/id.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/install.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/isort.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/java.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/javac.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/join.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/journalctl.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/jq.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/kill.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/killall.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/kubectl.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/less.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ln.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ls.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/make.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/md5sum.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mkdir.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/more.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mv.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mvn.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mvnw.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mypy.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/netstat.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nice.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nl.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/node.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nohup.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/npm.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/npx.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nslookup.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/paste.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ping.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pip.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pip3.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pipx.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pkill.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/poetry.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/prettier.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/printenv.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/printf.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ps.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pup.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pwd.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pytest.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/python.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/python3.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/read.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/readlink.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/realpath.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rev.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rg.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rm.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rsync.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ruff.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/scp.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sed.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/seq.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sh.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sort.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ss.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ssh.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/stat.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/strace.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sudo.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/systemctl.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tac.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tail.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tar.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/terraform.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/test.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/time.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/timeout.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/top.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/touch.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tr.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/traceroute.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/true.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tsc.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/type.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uname.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/unexpand.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uniq.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/unzip.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uptime.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uv.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/wc.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/wget.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/whereis.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/which.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/whoami.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/xargs.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/xmllint.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/yarn.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/yes.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/yq.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/zip.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/zsh.yaml +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/parser.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/py.typed +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/__init__.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/conftest.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_cli.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_matcher.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_models.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_parser.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_real_world.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_schema.py +0 -0
- {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_smoke.py +0 -0
- {bash_classify-0.4.2 → 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
|
|
|
@@ -246,7 +246,21 @@ Certain expression-level patterns elevate risk to at least `HIGH`:
|
|
|
246
246
|
- Writing to system paths (e.g. `/etc`, `/usr`)
|
|
247
247
|
- Redirecting to `/dev/tcp` or `/dev/udp`
|
|
248
248
|
- Backgrounding (`cmd &`)
|
|
249
|
-
- Output redirects to non-`/dev/null` targets
|
|
249
|
+
- Output redirects to non-`/dev/null` targets (except when all write targets are under `/tmp` or `/var/tmp`)
|
|
250
|
+
|
|
251
|
+
#### Temp path risk lowering
|
|
252
|
+
|
|
253
|
+
When all write targets from output redirects (`>`, `>>`, `2>`, `&>`, `>&`) are under `/tmp` or `/var/tmp`, the risk elevation to `MEDIUM` from output redirects is skipped. This allows commands like `cat > /tmp/foo.txt` to remain at `LOW` risk while still being classified as `LOCAL_EFFECTS`.
|
|
254
|
+
|
|
255
|
+
### File path detection
|
|
256
|
+
|
|
257
|
+
The tool extracts file paths from shell redirects:
|
|
258
|
+
|
|
259
|
+
- **`write_paths`**: Files targeted by output redirects (`>`, `>>`, `2>`, `&>`, `>&`), excluding `/dev/null`, `/dev/stdin`, `/dev/stdout`, `/dev/stderr`, and `/dev/fd/*`.
|
|
260
|
+
- **`read_paths`**: Files targeted by input redirects (`<`), excluding the same `/dev/*` paths.
|
|
261
|
+
- Heredoc operators (`<<`, `<<<`) are not included — their target is a delimiter, not a file path.
|
|
262
|
+
|
|
263
|
+
Paths are reported at both the per-command level (`commands[].write_paths`, `commands[].read_paths`) and the expression level (aggregated from all commands).
|
|
250
264
|
|
|
251
265
|
### Composite classification
|
|
252
266
|
|
|
@@ -918,6 +932,8 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
918
932
|
"expression": "string — the original input",
|
|
919
933
|
"classification": "READONLY | LOCAL_EFFECTS | EXTERNAL_EFFECTS | DANGEROUS | UNKNOWN",
|
|
920
934
|
"directories": ["string — detected directories"],
|
|
935
|
+
"write_paths": ["string — files targeted by output redirects (optional, omitted when empty)"],
|
|
936
|
+
"read_paths": ["string — files targeted by input redirects (optional, omitted when empty)"],
|
|
921
937
|
"commands": [
|
|
922
938
|
{
|
|
923
939
|
"command": ["string — binary + subcommand chain (without inner command tokens)"],
|
|
@@ -928,6 +944,8 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
928
944
|
"remaining_options": ["string — options that were not in database"],
|
|
929
945
|
"classification_reason": "string — why this classification was chosen",
|
|
930
946
|
"overriding_option": "string | null — the option that elevated classification",
|
|
947
|
+
"write_paths": ["string — files targeted by output redirects (optional, omitted when empty)"],
|
|
948
|
+
"read_paths": ["string — files targeted by input redirects (optional, omitted when empty)"],
|
|
931
949
|
"inner_commands": [
|
|
932
950
|
{
|
|
933
951
|
"delegation_mode": "string — how this inner command was extracted",
|
|
@@ -453,7 +453,15 @@ subcommands:
|
|
|
453
453
|
- Unrecognized goals (e.g. custom Maven plugin goals like `some-plugin:goal`) fall back to the command's base classification and risk
|
|
454
454
|
- Option overrides (e.g. `--dry-run`) still take precedence over goal aggregation
|
|
455
455
|
|
|
456
|
-
## 10.
|
|
456
|
+
## 10. Temp Path Risk Behavior
|
|
457
|
+
|
|
458
|
+
When a command writes to a file via output redirects (`>`, `>>`, `2>`, `&>`, `>&`), its risk is normally elevated to at least `MEDIUM`. However, if **all** write targets are under `/tmp` or `/var/tmp`, the risk elevation is skipped and the command stays at `LOW` risk (assuming no other risk elevations apply).
|
|
459
|
+
|
|
460
|
+
This means `cat > /tmp/foo.txt` is classified as `LOCAL_EFFECTS` with risk `LOW`, while `cat > ~/foo.txt` is classified as `LOCAL_EFFECTS` with risk `MEDIUM`.
|
|
461
|
+
|
|
462
|
+
The tool also reports `write_paths` and `read_paths` in the output, extracted from redirect operators. These fields are omitted from the JSON output when empty.
|
|
463
|
+
|
|
464
|
+
## 11. Common Patterns
|
|
457
465
|
|
|
458
466
|
| Pattern | Example | Classification |
|
|
459
467
|
|---------|---------|---------------|
|
|
@@ -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
|
},
|
|
@@ -48,6 +48,22 @@ _SAFE_PREFIXES = (
|
|
|
48
48
|
"/dev/udp",
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
_WRITE_OPERATORS = {">", ">>", "2>", "&>", ">&"}
|
|
52
|
+
_READ_OPERATORS = {"<"}
|
|
53
|
+
_HEREDOC_OPERATORS = {"<<", "<<<"}
|
|
54
|
+
|
|
55
|
+
_DEV_PATHS = ("/dev/null", "/dev/stdin", "/dev/stdout", "/dev/stderr", "/dev/fd/")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_temp_path(path: str) -> bool:
|
|
59
|
+
"""Check if a path is under a temp directory."""
|
|
60
|
+
return path.startswith("/tmp/") or path == "/tmp" or path.startswith("/var/tmp/") or path == "/var/tmp"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_real_file_path(path: str) -> bool:
|
|
64
|
+
"""Check if a redirect target is a real file path (not /dev/null etc)."""
|
|
65
|
+
return all(not (path == dev or path.startswith(dev)) for dev in _DEV_PATHS)
|
|
66
|
+
|
|
51
67
|
|
|
52
68
|
def _is_system_path(path: str) -> bool:
|
|
53
69
|
"""Check if a path is a system directory that should trigger DANGEROUS when written to."""
|
|
@@ -103,6 +119,20 @@ def classify_expression(
|
|
|
103
119
|
else:
|
|
104
120
|
result = match_command(invocation, database)
|
|
105
121
|
|
|
122
|
+
# Collect file paths from redirects
|
|
123
|
+
write_paths: list[str] = []
|
|
124
|
+
read_paths: list[str] = []
|
|
125
|
+
all_write_targets_are_temp = True
|
|
126
|
+
|
|
127
|
+
for redirect in invocation.redirects:
|
|
128
|
+
if redirect.operator in _WRITE_OPERATORS and _is_real_file_path(redirect.target):
|
|
129
|
+
write_paths.append(redirect.target)
|
|
130
|
+
if not _is_temp_path(redirect.target):
|
|
131
|
+
all_write_targets_are_temp = False
|
|
132
|
+
elif redirect.operator in _READ_OPERATORS and _is_real_file_path(redirect.target):
|
|
133
|
+
read_paths.append(redirect.target)
|
|
134
|
+
# Skip << and <<< (heredoc/herestring - target is delimiter, not file)
|
|
135
|
+
|
|
106
136
|
# Step 4: Apply redirect classification
|
|
107
137
|
for redirect in invocation.redirects:
|
|
108
138
|
if redirect.affects_classification:
|
|
@@ -114,8 +144,9 @@ def classify_expression(
|
|
|
114
144
|
if result.classification_reason
|
|
115
145
|
else "elevated by output redirect"
|
|
116
146
|
)
|
|
117
|
-
#
|
|
118
|
-
|
|
147
|
+
# Only elevate risk if write targets are NOT all temp paths
|
|
148
|
+
if not (write_paths and all_write_targets_are_temp):
|
|
149
|
+
result.risk = Risk.max_severity(result.risk, Risk.MEDIUM)
|
|
119
150
|
# /dev/tcp and /dev/udp redirects are network access -> DANGEROUS
|
|
120
151
|
if redirect.target.startswith("/dev/tcp/") or redirect.target.startswith("/dev/udp/"):
|
|
121
152
|
elevated = Classification.max_severity(result.classification, Classification.DANGEROUS)
|
|
@@ -165,12 +196,24 @@ def classify_expression(
|
|
|
165
196
|
) + f"; elevated to DANGEROUS: system path {system_paths_found[0]}"
|
|
166
197
|
result.risk = Risk.HIGH
|
|
167
198
|
|
|
199
|
+
result.write_paths = write_paths if write_paths else None
|
|
200
|
+
result.read_paths = read_paths if read_paths else None
|
|
201
|
+
|
|
168
202
|
command_results.append(result)
|
|
169
203
|
all_redirects.extend(invocation.redirects)
|
|
170
204
|
|
|
171
205
|
# Step 7: Collect directories
|
|
172
206
|
directories = _collect_directories(command_results)
|
|
173
207
|
|
|
208
|
+
# Step 7b: Aggregate write_paths and read_paths
|
|
209
|
+
all_write_paths: list[str] = []
|
|
210
|
+
all_read_paths: list[str] = []
|
|
211
|
+
for cmd_result in command_results:
|
|
212
|
+
if cmd_result.write_paths:
|
|
213
|
+
all_write_paths.extend(cmd_result.write_paths)
|
|
214
|
+
if cmd_result.read_paths:
|
|
215
|
+
all_read_paths.extend(cmd_result.read_paths)
|
|
216
|
+
|
|
174
217
|
# Step 8: Compute composite classification and risk
|
|
175
218
|
if not command_results and parse_warnings:
|
|
176
219
|
overall = Classification.UNKNOWN
|
|
@@ -187,6 +230,8 @@ def classify_expression(
|
|
|
187
230
|
classification=overall,
|
|
188
231
|
risk=overall_risk,
|
|
189
232
|
directories=directories,
|
|
233
|
+
write_paths=all_write_paths,
|
|
234
|
+
read_paths=all_read_paths,
|
|
190
235
|
commands=command_results,
|
|
191
236
|
redirects=all_redirects,
|
|
192
237
|
parse_warnings=parse_warnings,
|
|
@@ -57,6 +57,11 @@ def _command_to_dict(result: CommandResult) -> dict:
|
|
|
57
57
|
if result.overriding_option is not None:
|
|
58
58
|
d["overriding_option"] = result.overriding_option
|
|
59
59
|
|
|
60
|
+
if result.write_paths:
|
|
61
|
+
d["write_paths"] = result.write_paths
|
|
62
|
+
if result.read_paths:
|
|
63
|
+
d["read_paths"] = result.read_paths
|
|
64
|
+
|
|
60
65
|
d["inner_commands"] = [_inner_command_to_dict(ic) for ic in result.inner_commands]
|
|
61
66
|
|
|
62
67
|
return d
|
|
@@ -81,6 +86,11 @@ def _result_to_dict(result: ExpressionResult) -> dict:
|
|
|
81
86
|
"commands": [_command_to_dict(cmd) for cmd in result.commands],
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
if result.write_paths:
|
|
90
|
+
d["write_paths"] = result.write_paths
|
|
91
|
+
if result.read_paths:
|
|
92
|
+
d["read_paths"] = result.read_paths
|
|
93
|
+
|
|
84
94
|
if result.redirects:
|
|
85
95
|
d["redirects"] = [_redirect_to_dict(r) for r in result.redirects]
|
|
86
96
|
|
|
@@ -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)
|
|
@@ -177,6 +178,8 @@ class CommandResult:
|
|
|
177
178
|
classification_reason: str | None = None
|
|
178
179
|
overriding_option: str | None = None
|
|
179
180
|
directories: list[str] | None = None
|
|
181
|
+
write_paths: list[str] | None = None
|
|
182
|
+
read_paths: list[str] | None = None
|
|
180
183
|
|
|
181
184
|
|
|
182
185
|
@dataclass
|
|
@@ -187,6 +190,8 @@ class ExpressionResult:
|
|
|
187
190
|
classification: Classification
|
|
188
191
|
risk: Risk
|
|
189
192
|
directories: list[str]
|
|
193
|
+
write_paths: list[str]
|
|
194
|
+
read_paths: list[str]
|
|
190
195
|
commands: list[CommandResult]
|
|
191
196
|
redirects: list[Redirect]
|
|
192
197
|
parse_warnings: list[str]
|