bash-classify 0.9.1__tar.gz → 0.11.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.9.1 → bash_classify-0.11.0}/.claude-plugin/marketplace.json +1 -1
- {bash_classify-0.9.1 → bash_classify-0.11.0}/PKG-INFO +98 -2
- {bash_classify-0.9.1 → bash_classify-0.11.0}/README.md +96 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/SPEC.md +166 -15
- {bash_classify-0.9.1 → bash_classify-0.11.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
- {bash_classify-0.9.1 → bash_classify-0.11.0}/docs/classification-guidance.md +52 -5
- {bash_classify-0.9.1 → bash_classify-0.11.0}/schemas/command.schema.json +14 -1
- bash_classify-0.11.0/schemas/match-rules.schema.json +57 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/__init__.py +9 -1
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/classifier.py +30 -2
- bash_classify-0.11.0/src/bash_classify/cli.py +250 -0
- bash_classify-0.11.0/src/bash_classify/commands/eval.yaml +8 -0
- bash_classify-0.11.0/src/bash_classify/commands/exec.yaml +12 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/glab.yaml +67 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/database.py +50 -4
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/matcher.py +103 -16
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/models.py +13 -1
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/parser.py +118 -2
- bash_classify-0.11.0/src/bash_classify/rules.py +248 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-award-emoji.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-include-diverged.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-itself.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-pipelines-list.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/api-merge-request-update-description.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/api-project-lookup.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/ci-list.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/comment-only-mention.sh +2 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/echo-mentions-api-endpoint.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/echo-mentions-command.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/git-commit-heredoc-message.sh +8 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/git-commit-inline-message.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/glab-discussion-read-and-api-mr.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/glab-discussion-read.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/glab-pipeline-inspect.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/grep-pattern-mentions-command.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/grep-recursive-for-command.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/heredoc-brief-2026-09-04.sh +88 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/heredoc-brief-mentions-blocked-commands.sh +14 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/heredoc-report-mentions-blocked.sh +10 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/heredoc-skill-custom-delimiter.sh +11 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/heredoc-skill-phrase-split-across-lines.sh +10 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/hook-self-test-json-payload.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/loop-over-quoted-command-strings.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-note-list-output-json.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-note-list.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-update-then-view.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-view-bare.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-view-grep-pipeline.sh +2 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-view-output-equals-json.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-view-output-json.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/mr-view-output-short-flag.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/printf-mentions-command.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/python-heredoc-triple-quoted-mention.sh +15 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/allow/ripgrep-pattern-args.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-absolute-binary-path.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-delete-note-nested-api.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-cd-and-prefix.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-export-host-loop.sh +8 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-for-loop.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-heredoc-then-call.sh +8 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-hostname-python-consumer.sh +9 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-line-continuation.sh +2 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-method-get-field.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-output-ndjson.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-paginate-before-path.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-paginate.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-per-page-query.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-quoted-paginate-jq.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-redirect-to-file.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-repo-flag-first.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-discussions-var-capture.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-edit-note-put-heredoc.sh +7 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-new-diff-note-input-stdin.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-new-diff-note-position-fields.sh +6 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-note-award-emoji.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-note-by-id-or-retry.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-notes-after-allowed-mr-view.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-notes-query-sort.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-notes-timeout-prefix.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-reply-method-post-after-path.sh +6 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-reply-post-body-from-file.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-reply-post-heredoc-body.sh +8 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-resolve-loop.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-resolve-put-field.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/api-single-discussion-by-id.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/heredoc-then-and-chain-to-blocked.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/heredoc-then-pipe-to-blocked.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-custom-heredoc-delimiter.sh +8 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-help.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-heredoc-body.sh +7 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-inline-after-mr-update.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-long-message-flag.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-repo-before-message.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-second-in-pipeline.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-note-then-close.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-bare.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-loop.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-no-iid.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-page-flag.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-repo-flag.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-short-cluster.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/mr-view-comments-short-flag.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/wrapper-bash-c.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/block/wrapper-sudo.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-discussion/rules.yaml +17 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-job-metadata-without-trace.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-job-metadata-xargs.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-merge-request-itself.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-merge-request-pipelines-list-per-page.sh +5 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-merge-request-pipelines-list.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-pipelines-list-bash-c.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-pipelines-list-ref.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/api-pipelines-list-single-quoted.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/ci-lint-local-file.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/ci-list-per-page.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/ci-list.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/ci-status-compact.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/ci-status-pipeline-id.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/comment-only-mention.sh +2 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/echo-mentions-ci-command.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/git-commit-heredoc-message.sh +9 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/git-commit-inline-message.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/glab-discussion-read.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/glab-pipeline-inspect.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/grep-pattern-mentions-ci.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/heredoc-brief-mentions-ci-commands.sh +16 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/heredoc-skill-mentions-ci.sh +10 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/loop-over-quoted-command-strings.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/printf-mentions-ci-command.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/allow/ripgrep-pattern-args.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-continuation-lines.sh +5 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-input-and-header.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-method-post-field.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-ci-lint-raw-field-cat.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-after-allowed-metadata.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-env-assignment.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-redirect-to-file.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-retry-loop.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-job-trace-xargs.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-detail.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-env-assignment.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-hostname-after-path.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-comment-and-loop.sh +5 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-list.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-nested-loop-timeout.sh +6 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-paginate-ndjson.sh +2 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-python-consumer.sh +6 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-jobs-scope-failed-loop.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-status-poll-loop.sh +6 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/api-pipeline-test-report.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-get-after-allowed-status.sh +2 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-get-branch-in-echo-chain.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-get-branch-ref-sleep-prefix.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-get-json-jq-sort-uniq.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-get-then-ci-trace.sh +4 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-get-var-capture-poll-loop.sh +9 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-get-with-job-details-repo.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-trace-job-name-with-pipeline.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-trace-pipeline-id-before-job-name.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-trace-redirect-to-file.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-trace-repo-before-job-id.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-view-help.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-view-job-web-or-api.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/ci-view-pipeline-id.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/heredoc-then-pipe-to-blocked.sh +3 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/pipe-get-alias.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/pipeline-view-alias.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/wrapper-bash-c.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/block/wrapper-sudo.sh +1 -0
- bash_classify-0.11.0/tests/fixtures/match/glab-pipeline/rules.yaml +26 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_classifier.py +130 -1
- bash_classify-0.11.0/tests/test_cli.py +255 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_cli_serialization.py +64 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_database.py +67 -0
- bash_classify-0.11.0/tests/test_match_fixtures.py +176 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_matcher.py +285 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_parser.py +98 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_real_world.py +166 -0
- bash_classify-0.11.0/tests/test_rules.py +443 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_schema.py +19 -0
- bash_classify-0.9.1/src/bash_classify/cli.py +0 -149
- bash_classify-0.9.1/tests/test_cli.py +0 -82
- {bash_classify-0.9.1 → bash_classify-0.11.0}/.claude/rules/command-database.md +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/.editorconfig +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/.github/workflows/ci.yml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/.github/workflows/publish.yml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/.gitignore +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/.python-version +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/CLAUDE.md +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/LICENSE +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/coding-agent-plugins/claude-code/hooks/hook-permission-request.sh +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/coding-agent-plugins/claude-code/hooks/hook-pre-tool-use.sh +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/pyproject.toml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/ruff.toml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/__main__.py +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/apt.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/awk.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/basename.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/bash.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/black.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/brew.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/builtin.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/cargo.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/cat.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/chgrp.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/chmod.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/chown.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/column.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/comm.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/command.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/cp.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/curl.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/cut.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/date.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/df.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/diff.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/dig.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/dirname.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/docker.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/du.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/echo.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/env.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/eslint.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/expand.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/false.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/file.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/find.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/fmt.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/fold.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/free.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gawk.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gcloud.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gh.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/git.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/go.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gog.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gradle.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gradlew.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/grep.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/groups.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gunzip.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gws.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/gzip.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/hadolint.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/head.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/helm.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/hostname.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/id.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/install.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/isort.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/java.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/javac.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/join.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/journalctl.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/jq.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/kill.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/killall.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/kubectl.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/less.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/ln.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/ls.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/make.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/md5sum.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/mise.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/mkdir.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/more.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/mv.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/mvn.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/mvnw.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/mypy.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/netstat.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/nice.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/nl.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/node.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/nohup.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/npm.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/npx.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/nslookup.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/paste.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/ping.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pip.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pip3.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pipx.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pkill.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pnpm.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/poetry.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/prettier.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/printenv.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/printf.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/ps.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pup.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pwd.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/pytest.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/python.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/python3.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/read.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/readlink.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/realpath.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/rev.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/rg.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/rm.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/rsync.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/rtk.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/ruff.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/scp.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/sed.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/seq.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/sh.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/slack.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/sleep.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/snip.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/sort.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/ss.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/ssh.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/stat.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/stdbuf.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/strace.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/strings.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/sudo.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/systemctl.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/tac.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/tail.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/tar.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/tee.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/terraform.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/test.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/time.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/timeout.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/top.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/touch.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/tr.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/traceroute.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/true.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/tsc.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/type.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/uname.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/unexpand.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/uniq.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/unzip.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/uptime.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/uv.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/wait.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/wc.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/wget.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/whereis.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/which.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/whoami.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/xargs.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/xmllint.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/yarn.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/yes.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/yq.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/zip.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/commands/zsh.yaml +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/src/bash_classify/py.typed +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/__init__.py +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/conftest.py +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_models.py +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/tests/test_smoke.py +0 -0
- {bash_classify-0.9.1 → bash_classify-0.11.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: bash-classify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -61,6 +61,66 @@ $ echo 'find . -name "*.pyc" -delete' | bash-classify | jq '.classification'
|
|
|
61
61
|
"DANGEROUS"
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
## Matching command shapes
|
|
65
|
+
|
|
66
|
+
Classification answers "how risky is this?". A deny hook usually has a narrower question:
|
|
67
|
+
"does this expression run command shape X?" `bash-classify match` answers that one. It
|
|
68
|
+
parses the expression, walks every invocation at every depth, and reports which of the
|
|
69
|
+
shapes in a rules file were actually invoked — so a heredoc body, an `echo` string, a
|
|
70
|
+
`#` comment or a `grep` pattern that merely *names* the command does not count.
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
# blocked-commands.yaml
|
|
74
|
+
rules:
|
|
75
|
+
- name: mr-discussions-api
|
|
76
|
+
command: [glab, api]
|
|
77
|
+
any_arg_matches: 'merge_requests/[^/?]+/(discussions|notes)(/|\?|$)'
|
|
78
|
+
|
|
79
|
+
- name: mr-view-comments
|
|
80
|
+
command: [glab, mr, view]
|
|
81
|
+
any_option: [--comments, -c]
|
|
82
|
+
|
|
83
|
+
- name: mr-note
|
|
84
|
+
command: [glab, mr, note]
|
|
85
|
+
except: [[glab, mr, note, list]]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
$ echo 'sudo glab mr note 42 -m hi' | bash-classify match --rules blocked-commands.yaml
|
|
90
|
+
{
|
|
91
|
+
"matches": [
|
|
92
|
+
{
|
|
93
|
+
"rule": "mr-note",
|
|
94
|
+
"command": ["glab", "mr", "note"],
|
|
95
|
+
"argv": ["glab", "mr", "note", "42", "-m", "hi"],
|
|
96
|
+
"via": ["sudo"]
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"parse_warnings": []
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Within one rule every condition given must hold; rules are independent of each other, and
|
|
104
|
+
one invocation can match several. `command` is a prefix match against the *resolved*
|
|
105
|
+
command path, so `/usr/bin/glab --repo x mr note` still resolves to `glab mr note`.
|
|
106
|
+
`any_option` looks at the options actually present, with values stripped and declared
|
|
107
|
+
clusters expanded (`-wc` carries `-c`). `any_arg_matches` is a Python `re.search` over
|
|
108
|
+
every argument token — a pattern written for `grep -E` needs `\S` rather than
|
|
109
|
+
`[^[:space:]]`. `via` lists the enclosing wrappers, outermost first.
|
|
110
|
+
|
|
111
|
+
**Two things a caller has to check.** First, `parse_warnings` is always present: when it
|
|
112
|
+
is non-empty the expression could not be fully parsed, so an empty `matches` proves nothing
|
|
113
|
+
and the caller should fall back to whatever it did before. Second, check that the output
|
|
114
|
+
actually has a `matches` key. A `bash-classify` older than this mode does not reject the
|
|
115
|
+
unknown `match` argument — it ignores it, classifies stdin and exits 0, so the caller gets
|
|
116
|
+
a normal classification JSON with no `matches` key. Treat a non-zero exit, unparseable
|
|
117
|
+
output, or output without a `matches` key as "cannot answer" and fall back; never read a
|
|
118
|
+
missing `matches` as "nothing matched".
|
|
119
|
+
|
|
120
|
+
Exit codes are `0` whether or not anything matched, `1` for empty input or no input within
|
|
121
|
+
5 seconds, and `2` for bad arguments, an unreadable or invalid rules file, or an internal
|
|
122
|
+
error.
|
|
123
|
+
|
|
64
124
|
## Claude Code plugin
|
|
65
125
|
|
|
66
126
|
The repo includes a Claude Code plugin that auto-allows low-risk bash commands via a `PreToolUse` hook.
|
|
@@ -120,6 +180,7 @@ Risk defaults are derived from classification (`READONLY`→LOW, `LOCAL_EFFECTS`
|
|
|
120
180
|
- **Tree-sitter parsing** -- bash expressions are parsed into an AST for accurate command extraction, handling pipes, subshells, and command substitution
|
|
121
181
|
- **YAML command database** -- each command has classification rules with subcommand and option matching
|
|
122
182
|
- **Subcommand matching** -- `kubectl get` and `kubectl delete` can have different classifications
|
|
183
|
+
- **Subcommand aliases** -- a subcommand can declare other names for itself, so `glab pipeline view` resolves to `glab ci view` and any rule written for the canonical name catches the aliased spelling
|
|
123
184
|
- **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
|
|
124
185
|
- **Delegation for wrappers** -- commands like `xargs`, `sudo`, and `env` delegate classification to the inner command
|
|
125
186
|
- **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
|
|
@@ -134,6 +195,41 @@ print(result.classification) # Classification.READONLY
|
|
|
134
195
|
print(result.risk) # Risk.LOW
|
|
135
196
|
```
|
|
136
197
|
|
|
198
|
+
Each command result carries the options it actually uses and the positionals left after parsing. Option values
|
|
199
|
+
are stripped, so `--key=value` shows up as `--key` and `-fvalue` as `-f`:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
command = classify_expression("git commit --amend -m 'wip'").commands[0]
|
|
203
|
+
print(command.command) # ['git', 'commit']
|
|
204
|
+
print(command.options) # ['--amend', '-m']
|
|
205
|
+
print(command.positionals) # []
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`iter_invocations` walks every invocation in an expression depth-first — top-level commands and, recursively,
|
|
209
|
+
the inner commands that wrappers such as `sudo`, `timeout` or `bash -c` delegate to. It yields each invocation
|
|
210
|
+
with its `via` chain: the enclosing wrappers, outermost first, empty at the top level.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from bash_classify import classify_expression, iter_invocations
|
|
214
|
+
|
|
215
|
+
for invocation, via in iter_invocations(classify_expression("sudo timeout 5 ls")):
|
|
216
|
+
print(via, invocation.command)
|
|
217
|
+
# [] ['sudo']
|
|
218
|
+
# ['sudo'] ['timeout']
|
|
219
|
+
# ['sudo', 'timeout'] ['ls']
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`load_rules` and `match_expression` are the same thing from Python:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from bash_classify import load_rules, match_expression
|
|
226
|
+
|
|
227
|
+
rules = load_rules("blocked-commands.yaml")
|
|
228
|
+
result = match_expression('cat > brief.md <<"EOF"\nmentions glab mr note\nEOF', rules)
|
|
229
|
+
print(result.matches) # [] - the heredoc body is data, not a command
|
|
230
|
+
print(result.parse_warnings) # []
|
|
231
|
+
```
|
|
232
|
+
|
|
137
233
|
See [SPEC.md](SPEC.md) for the full specification.
|
|
138
234
|
|
|
139
235
|
## Development
|
|
@@ -32,6 +32,66 @@ $ echo 'find . -name "*.pyc" -delete' | bash-classify | jq '.classification'
|
|
|
32
32
|
"DANGEROUS"
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Matching command shapes
|
|
36
|
+
|
|
37
|
+
Classification answers "how risky is this?". A deny hook usually has a narrower question:
|
|
38
|
+
"does this expression run command shape X?" `bash-classify match` answers that one. It
|
|
39
|
+
parses the expression, walks every invocation at every depth, and reports which of the
|
|
40
|
+
shapes in a rules file were actually invoked — so a heredoc body, an `echo` string, a
|
|
41
|
+
`#` comment or a `grep` pattern that merely *names* the command does not count.
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
# blocked-commands.yaml
|
|
45
|
+
rules:
|
|
46
|
+
- name: mr-discussions-api
|
|
47
|
+
command: [glab, api]
|
|
48
|
+
any_arg_matches: 'merge_requests/[^/?]+/(discussions|notes)(/|\?|$)'
|
|
49
|
+
|
|
50
|
+
- name: mr-view-comments
|
|
51
|
+
command: [glab, mr, view]
|
|
52
|
+
any_option: [--comments, -c]
|
|
53
|
+
|
|
54
|
+
- name: mr-note
|
|
55
|
+
command: [glab, mr, note]
|
|
56
|
+
except: [[glab, mr, note, list]]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ echo 'sudo glab mr note 42 -m hi' | bash-classify match --rules blocked-commands.yaml
|
|
61
|
+
{
|
|
62
|
+
"matches": [
|
|
63
|
+
{
|
|
64
|
+
"rule": "mr-note",
|
|
65
|
+
"command": ["glab", "mr", "note"],
|
|
66
|
+
"argv": ["glab", "mr", "note", "42", "-m", "hi"],
|
|
67
|
+
"via": ["sudo"]
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
"parse_warnings": []
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Within one rule every condition given must hold; rules are independent of each other, and
|
|
75
|
+
one invocation can match several. `command` is a prefix match against the *resolved*
|
|
76
|
+
command path, so `/usr/bin/glab --repo x mr note` still resolves to `glab mr note`.
|
|
77
|
+
`any_option` looks at the options actually present, with values stripped and declared
|
|
78
|
+
clusters expanded (`-wc` carries `-c`). `any_arg_matches` is a Python `re.search` over
|
|
79
|
+
every argument token — a pattern written for `grep -E` needs `\S` rather than
|
|
80
|
+
`[^[:space:]]`. `via` lists the enclosing wrappers, outermost first.
|
|
81
|
+
|
|
82
|
+
**Two things a caller has to check.** First, `parse_warnings` is always present: when it
|
|
83
|
+
is non-empty the expression could not be fully parsed, so an empty `matches` proves nothing
|
|
84
|
+
and the caller should fall back to whatever it did before. Second, check that the output
|
|
85
|
+
actually has a `matches` key. A `bash-classify` older than this mode does not reject the
|
|
86
|
+
unknown `match` argument — it ignores it, classifies stdin and exits 0, so the caller gets
|
|
87
|
+
a normal classification JSON with no `matches` key. Treat a non-zero exit, unparseable
|
|
88
|
+
output, or output without a `matches` key as "cannot answer" and fall back; never read a
|
|
89
|
+
missing `matches` as "nothing matched".
|
|
90
|
+
|
|
91
|
+
Exit codes are `0` whether or not anything matched, `1` for empty input or no input within
|
|
92
|
+
5 seconds, and `2` for bad arguments, an unreadable or invalid rules file, or an internal
|
|
93
|
+
error.
|
|
94
|
+
|
|
35
95
|
## Claude Code plugin
|
|
36
96
|
|
|
37
97
|
The repo includes a Claude Code plugin that auto-allows low-risk bash commands via a `PreToolUse` hook.
|
|
@@ -91,6 +151,7 @@ Risk defaults are derived from classification (`READONLY`→LOW, `LOCAL_EFFECTS`
|
|
|
91
151
|
- **Tree-sitter parsing** -- bash expressions are parsed into an AST for accurate command extraction, handling pipes, subshells, and command substitution
|
|
92
152
|
- **YAML command database** -- each command has classification rules with subcommand and option matching
|
|
93
153
|
- **Subcommand matching** -- `kubectl get` and `kubectl delete` can have different classifications
|
|
154
|
+
- **Subcommand aliases** -- a subcommand can declare other names for itself, so `glab pipeline view` resolves to `glab ci view` and any rule written for the canonical name catches the aliased spelling
|
|
94
155
|
- **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
|
|
95
156
|
- **Delegation for wrappers** -- commands like `xargs`, `sudo`, and `env` delegate classification to the inner command
|
|
96
157
|
- **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
|
|
@@ -105,6 +166,41 @@ print(result.classification) # Classification.READONLY
|
|
|
105
166
|
print(result.risk) # Risk.LOW
|
|
106
167
|
```
|
|
107
168
|
|
|
169
|
+
Each command result carries the options it actually uses and the positionals left after parsing. Option values
|
|
170
|
+
are stripped, so `--key=value` shows up as `--key` and `-fvalue` as `-f`:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
command = classify_expression("git commit --amend -m 'wip'").commands[0]
|
|
174
|
+
print(command.command) # ['git', 'commit']
|
|
175
|
+
print(command.options) # ['--amend', '-m']
|
|
176
|
+
print(command.positionals) # []
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`iter_invocations` walks every invocation in an expression depth-first — top-level commands and, recursively,
|
|
180
|
+
the inner commands that wrappers such as `sudo`, `timeout` or `bash -c` delegate to. It yields each invocation
|
|
181
|
+
with its `via` chain: the enclosing wrappers, outermost first, empty at the top level.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from bash_classify import classify_expression, iter_invocations
|
|
185
|
+
|
|
186
|
+
for invocation, via in iter_invocations(classify_expression("sudo timeout 5 ls")):
|
|
187
|
+
print(via, invocation.command)
|
|
188
|
+
# [] ['sudo']
|
|
189
|
+
# ['sudo'] ['timeout']
|
|
190
|
+
# ['sudo', 'timeout'] ['ls']
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`load_rules` and `match_expression` are the same thing from Python:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from bash_classify import load_rules, match_expression
|
|
197
|
+
|
|
198
|
+
rules = load_rules("blocked-commands.yaml")
|
|
199
|
+
result = match_expression('cat > brief.md <<"EOF"\nmentions glab mr note\nEOF', rules)
|
|
200
|
+
print(result.matches) # [] - the heredoc body is data, not a command
|
|
201
|
+
print(result.parse_warnings) # []
|
|
202
|
+
```
|
|
203
|
+
|
|
108
204
|
See [SPEC.md](SPEC.md) for the full specification.
|
|
109
205
|
|
|
110
206
|
## Development
|
|
@@ -186,12 +186,90 @@ $ echo 'sh -c "ls /tmp | grep log"' | bash-classify
|
|
|
186
186
|
}
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
### match mode
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
$ echo 'sudo glab mr note 42 -m hi' | bash-classify match --rules blocked-commands.yaml
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"matches": [
|
|
198
|
+
{
|
|
199
|
+
"rule": "mr-note",
|
|
200
|
+
"command": ["glab", "mr", "note"],
|
|
201
|
+
"argv": ["glab", "mr", "note", "42", "-m", "hi"],
|
|
202
|
+
"via": ["sudo"]
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
"parse_warnings": []
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`match` answers a narrower question than classification: which of the command shapes
|
|
210
|
+
declared in a rules file does this expression actually invoke? Shell text that only names
|
|
211
|
+
a command — a heredoc body, an `echo` or `printf` argument, a `#` comment, a
|
|
212
|
+
`git commit -m` message, a `grep` pattern — is not an invocation and does not match.
|
|
213
|
+
|
|
214
|
+
#### The rules file
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
rules:
|
|
218
|
+
- name: mr-note # required, unique within the file
|
|
219
|
+
command: [glab, mr, note] # required; prefix match on the resolved path
|
|
220
|
+
except: [[glab, mr, note, list]] # optional; prefix paths excluded from the match
|
|
221
|
+
any_option: [--comments, -c] # optional; at least one of these is present
|
|
222
|
+
any_arg_matches: 'merge_requests/[^/?]+/notes' # optional; Python re.search
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Within one rule every condition that is given must hold (AND). Rules are independent of
|
|
226
|
+
each other (OR), and one invocation may match several rules; each match is reported.
|
|
227
|
+
|
|
228
|
+
| Field | Meaning |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `name` | Reported back on every match; must be unique within the file |
|
|
231
|
+
| `command` | Prefix match against `command` — the *resolved* path, so `/usr/bin/glab --repo x mr note` matches `[glab, mr, note]` |
|
|
232
|
+
| `except` | Prefix paths excluded from the match, so `[glab, mr, note]` can spare `glab mr note list` |
|
|
233
|
+
| `any_option` | At least one of these must appear in `options`: as typed, values stripped, `--key=value` counting as `--key` and a declared cluster `-wc` counting as `-w` and `-c`. Tokens after `--` are positionals and never match |
|
|
234
|
+
| `any_arg_matches` | Python `re.search` applied to every argument token: `argv[1:]` with the resolved subcommand words removed once each. Option flags, option values and positionals are all included; `argv[0]` and the subcommand words are not. A pattern written for `grep -E` needs `\S` instead of `[^[:space:]]` |
|
|
235
|
+
|
|
236
|
+
A rule matches an invocation at any depth: top-level commands and, recursively, the inner
|
|
237
|
+
commands that wrappers delegate to (`sudo`, `timeout`, `xargs`, `bash -c`, `find -exec`,
|
|
238
|
+
`eval`, `exec`). Commands inside `$(...)` are top-level invocations in their own right.
|
|
239
|
+
`via` lists the enclosing wrappers, outermost first, each as its resolved path joined by
|
|
240
|
+
spaces; it is empty at the top level.
|
|
241
|
+
|
|
242
|
+
`parse_warnings` is **always** present in match output, even when empty, because a caller
|
|
243
|
+
has to check it: a non-empty list means the expression could not be fully parsed, so an
|
|
244
|
+
empty `matches` list proves nothing.
|
|
245
|
+
|
|
246
|
+
#### Exit codes for `match`
|
|
247
|
+
|
|
248
|
+
- `0` — ran successfully, whether or not anything matched
|
|
249
|
+
- `1` — empty input, or no input on stdin within 5 seconds
|
|
250
|
+
- `2` — bad arguments, a missing or invalid rules file (the message names the file and the
|
|
251
|
+
offending rule), or an internal error
|
|
252
|
+
|
|
253
|
+
A caller cannot rely on the exit code alone to detect an old binary. Versions before this
|
|
254
|
+
mode parsed `sys.argv` by hand and ignored anything they did not recognize, so
|
|
255
|
+
`bash-classify match --rules f.yaml` on such a binary classifies stdin and exits `0`,
|
|
256
|
+
printing an ordinary classification document with no `matches` key. The check that works is
|
|
257
|
+
the output shape: require a `matches` key, and treat a non-zero exit, unparseable output or
|
|
258
|
+
a missing `matches` key alike as "cannot answer" — fall back rather than reading it as
|
|
259
|
+
"nothing matched".
|
|
260
|
+
|
|
189
261
|
### Exit codes
|
|
190
262
|
|
|
191
263
|
- `0` — successfully classified
|
|
192
|
-
- `1` —
|
|
264
|
+
- `1` — empty input, or no input arrived on stdin within 5 seconds
|
|
193
265
|
- `2` — internal error
|
|
194
266
|
|
|
267
|
+
A bash syntax error does **not** change the exit code. The parse is best-effort: the tool still exits `0`, records the
|
|
268
|
+
problem in `parse_warnings`, and returns whatever commands it could extract from the partial parse. Consumers rely on
|
|
269
|
+
this — check `parse_warnings` to decide whether the `commands` list can be trusted, not the exit code.
|
|
270
|
+
|
|
271
|
+
`match` mode has its own, slightly different set — see [Exit codes for `match`](#exit-codes-for-match) above.
|
|
272
|
+
|
|
195
273
|
### Classification levels
|
|
196
274
|
|
|
197
275
|
| Level | Meaning |
|
|
@@ -299,7 +377,7 @@ The parser walks the CST and extracts a list of `CommandInvocation` objects, eac
|
|
|
299
377
|
| Command substitution `$(cmd)` | Inner command extracted recursively |
|
|
300
378
|
| Process substitution `<(cmd)` / `>(cmd)` | Inner command extracted recursively |
|
|
301
379
|
| Subshells `(cmd)` | Inner command extracted recursively |
|
|
302
|
-
| Heredocs `<<EOF` |
|
|
380
|
+
| Heredocs `<<EOF` | Body is dropped entirely — it is never parsed as commands and never appears in any `argv`; only the operator and the delimiter are recorded as a redirect. Commands written *after* the opener on the same line (`cat <<EOF \| wc -l`, `cat <<EOF && rm x`) are extracted like any other pipeline or list member — tree-sitter nests them inside the heredoc node, but they are real commands. A `;` or `&` in that position is a tree-sitter parse error: the follower cannot be recovered and a `parse_warnings` entry says so |
|
|
303
381
|
| Variable assignments `X=1 cmd` | Prefix assignments stripped, `cmd` extracted |
|
|
304
382
|
| Backgrounding `cmd &` | Classified as EXTERNAL_EFFECTS (side effect: background process) |
|
|
305
383
|
|
|
@@ -307,18 +385,20 @@ The parser walks the CST and extracts a list of `CommandInvocation` objects, eac
|
|
|
307
385
|
|
|
308
386
|
| Construct | Reason |
|
|
309
387
|
|---|---|
|
|
310
|
-
| `eval "..."` | Arbitrary code execution,
|
|
311
|
-
| `
|
|
312
|
-
| `source file` / `. file` | Executes external script |
|
|
388
|
+
| `eval "..."` | Arbitrary code execution. The argument is still parsed and the inner command exposed, but a `min_classification: DANGEROUS` floor keeps both the inner and `eval` itself DANGEROUS |
|
|
389
|
+
| `exec cmd` | Replaces the shell with `cmd`. The inner command is exposed, with the same DANGEROUS floor |
|
|
390
|
+
| `source file` / `. file` | Executes external script, whose contents are never visible |
|
|
313
391
|
| Unquoted variable expansion in command position (`$CMD args`) | Command identity unknown at parse time |
|
|
314
392
|
|
|
393
|
+
`sh` and `bash` are **not** in this table. Their base classification is DANGEROUS, which is what a bare `sh` or `bash` gets, but `-c` is a `flag_value_is_expression` delegation with no `min_classification`: the expression is parsed, the inner commands are exposed, and the wrapper base is erased, so `sh -c "ls"` classifies READONLY.
|
|
394
|
+
|
|
315
395
|
## Command Matching Algorithm
|
|
316
396
|
|
|
317
397
|
This is the core of the tool. Given an `argv` like `["kubectl", "--context=prod", "get", "pods", "-n", "kube-system"]`, the matcher must:
|
|
318
398
|
|
|
319
399
|
1. **Look up the binary** (`kubectl`) in the database
|
|
320
400
|
2. **Separate global options** from the rest of `argv`, using the database definition of which options are global and whether they take a value
|
|
321
|
-
3. **Match the subcommand chain** from the remaining tokens (e.g. `get` → resolve to `kubectl.get`)
|
|
401
|
+
3. **Match the subcommand chain** from the remaining tokens (e.g. `get` → resolve to `kubectl.get`), resolving any declared subcommand alias to its canonical name
|
|
322
402
|
4. **Classify remaining options** against the matched subcommand's option definitions
|
|
323
403
|
5. **Determine the final classification** based on subcommand default + any option overrides
|
|
324
404
|
|
|
@@ -371,6 +451,14 @@ The default mode. Subcommands are matched as a greedy chain through nested subco
|
|
|
371
451
|
|
|
372
452
|
Example: `kubectl rollout status` matches `kubectl` → `rollout` → `status`, walking the tree.
|
|
373
453
|
|
|
454
|
+
#### Subcommand aliases
|
|
455
|
+
|
|
456
|
+
A subcommand may declare `aliases`. Every alias is another name for the same definition, so matching a word against a subcommand dictionary tries the canonical names and the aliases alike. What is reported is always the canonical name: `glab pipeline view 1000` yields `command: ["glab", "ci", "view"]` and `matched_rule: "glab.ci.view"`, while `argv` keeps `["glab", "pipeline", "view", "1000"]` — the word that was actually typed. Classification, options and `strict` behaviour are the canonical subcommand's, since it is literally the same definition.
|
|
457
|
+
|
|
458
|
+
This matters to `match` mode: a rule written as `command: [glab, ci, view]` catches the aliased spelling too, without listing it.
|
|
459
|
+
|
|
460
|
+
Aliases live in one namespace with their siblings. An alias that names a sibling subcommand, or that two siblings both claim, is rejected when the database file is loaded — the error names the file and the alias.
|
|
461
|
+
|
|
374
462
|
#### `match_all`
|
|
375
463
|
|
|
376
464
|
Each positional argument is matched independently against the **same** top-level subcommand dictionary. This is designed for build tools like Maven and Gradle where multiple goals/tasks appear as positional arguments in any order.
|
|
@@ -841,6 +929,7 @@ The `delegates_to` field defines how a command (or option like `find -exec`) han
|
|
|
841
929
|
| `after_separator` | Everything after the `separator` token forms the inner argv. | `kubectl exec pod -- ls -la` → inner: `["ls", "-la"]` |
|
|
842
930
|
| `terminated_argv` | Tokens after the flag up to `terminator` form the inner argv. `{}` tokens are stripped (they are `find` placeholders). | `find . -exec rm {} \;` → inner: `["rm"]` |
|
|
843
931
|
| `flag_value_is_expression` | The value of the specified `flag` is a complete shell expression string, parsed from scratch through the bash parser (not just tokenized as argv). | `sh -c "ls \| grep foo"` → inner expression: `ls \| grep foo` (two piped commands) |
|
|
932
|
+
| `args_are_expression` | Every token after the command word is joined with single spaces and the result is parsed as a complete shell expression, the way `eval` concatenates its arguments before running them. Option-looking tokens are included, because a command using this mode has no options of its own. Invocations the parser cannot recover yield no inner command. | `eval "glab mr list"` and `eval glab mr list` → inner: `["glab", "mr", "list"]` |
|
|
844
933
|
|
|
845
934
|
#### Delegation fields
|
|
846
935
|
|
|
@@ -871,6 +960,14 @@ When command-level delegation resolves at least one inner command (and no option
|
|
|
871
960
|
| `subcommand_mode` | `enum` | How subcommands are matched: `hierarchical` (default, chained tree walk) or `match_all` (each positional arg matched independently) |
|
|
872
961
|
| `delegates_to` | `object` | How this command delegates execution to an inner command (see delegation modes) |
|
|
873
962
|
|
|
963
|
+
#### Subcommand level
|
|
964
|
+
|
|
965
|
+
A subcommand entry takes the same fields as the command level, minus `command` and `global_options` — its name is the key it is filed under — plus:
|
|
966
|
+
|
|
967
|
+
| Field | Type | Description |
|
|
968
|
+
|---|---|---|
|
|
969
|
+
| `aliases` | `list[str]` | Alternative names for this subcommand (e.g. `pipe` and `pipeline` for `glab ci`). Each alias resolves to this same definition; the resolved `command` path and `matched_rule` name the canonical subcommand, while `argv` keeps the word that was typed. An alias that collides with a sibling subcommand, or that two siblings both claim, is a load-time error. |
|
|
970
|
+
|
|
874
971
|
#### Option level
|
|
875
972
|
|
|
876
973
|
| Field | Type | Description |
|
|
@@ -909,14 +1006,15 @@ Directories are reported as-is (not resolved) since variable expansion may be in
|
|
|
909
1006
|
|
|
910
1007
|
## Special-cased Commands
|
|
911
1008
|
|
|
912
|
-
Most delegation behavior is expressed in the database via `delegates_to`. Only shell builtins that cannot be modeled as regular commands need hardcoded handling
|
|
1009
|
+
Most delegation behavior is expressed in the database via `delegates_to`. Only shell builtins that cannot be modeled as regular commands need hardcoded handling — the full set is three short lists in `matcher.py`:
|
|
913
1010
|
|
|
914
1011
|
| Command | Special handling |
|
|
915
1012
|
|---|---|
|
|
916
1013
|
| `cd`, `pushd`, `popd` | Directory tracking; classified as `READONLY` |
|
|
917
|
-
| `
|
|
918
|
-
| `source`, `.` | Always `DANGEROUS` — executes external script |
|
|
919
|
-
|
|
1014
|
+
| `[`, `[[`, `test` | Always `READONLY` — condition evaluation with no side effects |
|
|
1015
|
+
| `source`, `.` | Always `DANGEROUS` — executes an external script whose contents are never visible |
|
|
1016
|
+
|
|
1017
|
+
`eval` and `exec` are **not** in that set. They are ordinary database entries (`commands/eval.yaml`, `commands/exec.yaml`) that delegate: `eval` via `args_are_expression`, `exec` via `rest_are_argv`, each with `min_classification: DANGEROUS`. So their inner command is parsed and reported in `inner_commands`, while the floor keeps both the inner and the wrapper at `DANGEROUS`/`HIGH`.
|
|
920
1018
|
|
|
921
1019
|
Everything else (`sudo`, `env`, `xargs`, `sh -c`, `nice`, `nohup`, `timeout`, `time`, `find -exec`, `kubectl exec --`, etc.) is handled via `delegates_to` in the database — no special code needed.
|
|
922
1020
|
|
|
@@ -930,7 +1028,30 @@ Python 3.12+. Dependencies:
|
|
|
930
1028
|
|
|
931
1029
|
## Output Schema
|
|
932
1030
|
|
|
933
|
-
|
|
1031
|
+
This is the schema of the default mode. `match` mode has its own, much smaller one:
|
|
1032
|
+
|
|
1033
|
+
```json
|
|
1034
|
+
{
|
|
1035
|
+
"matches": [
|
|
1036
|
+
{
|
|
1037
|
+
"rule": "string — the name of the rule that matched",
|
|
1038
|
+
"command": ["string — the resolved command path of the invocation"],
|
|
1039
|
+
"argv": ["string — the full argv of the invocation"],
|
|
1040
|
+
"via": ["string — enclosing wrapper commands, outermost first; empty at top level"]
|
|
1041
|
+
}
|
|
1042
|
+
],
|
|
1043
|
+
"parse_warnings": ["string — non-fatal issues encountered during parsing"]
|
|
1044
|
+
}
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
Both keys are always present in match output, `matches` as `[]` when nothing matched and
|
|
1048
|
+
`parse_warnings` as `[]` on a clean parse. As in the default mode, `parse_warnings` covers
|
|
1049
|
+
nested expressions: a syntax error inside `bash -c "..."` or `eval "..."` surfaces here too.
|
|
1050
|
+
A caller must check both: a non-empty `parse_warnings` means an empty `matches` proves
|
|
1051
|
+
nothing, and a *missing* `matches` key means the output came from a binary that predates
|
|
1052
|
+
this mode.
|
|
1053
|
+
|
|
1054
|
+
The `commands` list below is recursive — any command entry can contain `inner_commands` when the command delegates execution to another command (via `delegates_to` in the database).
|
|
934
1055
|
|
|
935
1056
|
```json
|
|
936
1057
|
{
|
|
@@ -945,6 +1066,8 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
945
1066
|
"argv": ["string — full original argv for this command"],
|
|
946
1067
|
"classification": "READONLY | LOCAL_EFFECTS | EXTERNAL_EFFECTS | DANGEROUS | UNKNOWN",
|
|
947
1068
|
"matched_rule": "string — dotted path in database, or null",
|
|
1069
|
+
"options": ["string — every option present, canonical flag only, no values; always present, empty when none"],
|
|
1070
|
+
"positionals": ["string — non-option tokens left after subcommand and option parsing; always present, empty when none"],
|
|
948
1071
|
"ignored_options": ["string — global options that were stripped"],
|
|
949
1072
|
"remaining_options": ["string — options that were not in database"],
|
|
950
1073
|
"classification_reason": "string — why this classification was chosen",
|
|
@@ -959,6 +1082,8 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
959
1082
|
"argv": ["..."],
|
|
960
1083
|
"classification": "...",
|
|
961
1084
|
"matched_rule": "...",
|
|
1085
|
+
"options": ["... — same meaning as on the enclosing command"],
|
|
1086
|
+
"positionals": ["... — same meaning as on the enclosing command"],
|
|
962
1087
|
"inner_commands": ["... — recursive, can nest further"]
|
|
963
1088
|
}
|
|
964
1089
|
]
|
|
@@ -971,10 +1096,37 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
971
1096
|
"affects_classification": "bool"
|
|
972
1097
|
}
|
|
973
1098
|
],
|
|
974
|
-
"parse_warnings": ["string — non-fatal issues encountered during parsing"]
|
|
1099
|
+
"parse_warnings": ["string — non-fatal issues encountered during parsing. A non-empty list means the parse was best-effort and the commands list may be incomplete."]
|
|
975
1100
|
}
|
|
976
1101
|
```
|
|
977
1102
|
|
|
1103
|
+
`parse_warnings` covers **nested** expressions as well as the outer one. A syntax error inside
|
|
1104
|
+
`bash -c "..."`, `sh -c`, `zsh -c` or `eval "..."` surfaces here at any wrapper depth
|
|
1105
|
+
(`sudo bash -c '...'`, `find . -exec bash -c '...' \;`), and the warning text names the nested
|
|
1106
|
+
expression it came from, so a caller can tell which part of the command could not be read.
|
|
1107
|
+
Identical warnings are reported once.
|
|
1108
|
+
|
|
1109
|
+
#### `options` and `positionals`
|
|
1110
|
+
|
|
1111
|
+
`options` records which options an invocation actually carries, with option values stripped and each option in
|
|
1112
|
+
its canonical form: `--key=value` contributes `--key`, `-fvalue` contributes `-f`, and a cluster whose every
|
|
1113
|
+
character is declared in the database (`-wc`) contributes each character (`-w`, `-c`). Options the database does
|
|
1114
|
+
not model are included too — present means present. Tokens after `--` are positionals and contribute nothing.
|
|
1115
|
+
|
|
1116
|
+
Two consequences of matching against a database that models only part of a real CLI. In a cluster whose first
|
|
1117
|
+
character is declared but a later one is not (`-wx` with only `-w` declared), the undeclared characters are not
|
|
1118
|
+
reported anywhere — not in `options`, not in `remaining_options` — so a check against `options` can miss such an
|
|
1119
|
+
option but can never falsely fire on one. In the other direction, an option the database does not model but that
|
|
1120
|
+
really takes a value has its value reported as a second option (`-e -foo` yields `-e` and `-foo`, because nothing
|
|
1121
|
+
says `-e` consumes the next token), so `options` errs toward over-reporting for unmodelled options. Declaring the
|
|
1122
|
+
option in the database fixes both.
|
|
1123
|
+
|
|
1124
|
+
`positionals` holds the non-option tokens that remain once the subcommand chain and the options have been
|
|
1125
|
+
consumed, including `--` itself and everything after it.
|
|
1126
|
+
|
|
1127
|
+
Both are always serialized, as `[]` when the command has none. Results that never reach option parsing — shell
|
|
1128
|
+
builtins, an unknown binary, an empty argv — carry `null` internally and serialize as `[]`.
|
|
1129
|
+
|
|
978
1130
|
### Recursive classification rules
|
|
979
1131
|
|
|
980
1132
|
1. A command's `classification` is the **maximum** of:
|
|
@@ -990,14 +1142,13 @@ The `commands` list is recursive — any command entry can contain `inner_comman
|
|
|
990
1142
|
|
|
991
1143
|
## Non-goals
|
|
992
1144
|
|
|
993
|
-
- **Not a sandbox.** This is a classifier, not an enforcer. It does not execute or block anything.
|
|
1145
|
+
- **Not a sandbox.** This is a classifier, not an enforcer. It does not execute or block anything. `match` mode is no exception: it reports which declared command shapes an expression invokes, and the caller decides what to do about it.
|
|
994
1146
|
- **No awk/sed/perl script analysis.** These are classified as a whole command; their embedded programs are opaque. They should simply not be in the READONLY allowlist.
|
|
995
1147
|
- **No variable resolution.** `$DIR`, `$(cmd)` in command position → UNKNOWN. We classify what we can see statically.
|
|
996
|
-
- **No alias/function resolution.** We classify the literal command name as written.
|
|
1148
|
+
- **No shell alias/function resolution.** We classify the literal command name as written.
|
|
997
1149
|
|
|
998
1150
|
## Future extensions
|
|
999
1151
|
|
|
1000
|
-
- `--format=decision` — output only the classification string (for use in hooks)
|
|
1001
1152
|
- `--database=/path` — custom database directory
|
|
1002
1153
|
- `--explain` — verbose output showing the matching steps
|
|
1003
1154
|
- Interactive database builder — run `command --help` and generate a skeleton YAML definition
|
|
@@ -84,6 +84,25 @@ options: # options that affect classification
|
|
|
84
84
|
| `subcommands` | map | -- | Nested subcommand definitions (recursive) |
|
|
85
85
|
| `delegates_to` | object | -- | How the command hands off to an inner command |
|
|
86
86
|
|
|
87
|
+
#### Subcommand fields
|
|
88
|
+
|
|
89
|
+
A subcommand takes the same fields as the top level, minus `command`, `description` and `global_options`, plus:
|
|
90
|
+
|
|
91
|
+
| Field | Type | Description |
|
|
92
|
+
|-------|------|-------------|
|
|
93
|
+
| `aliases` | list | Alternative names for this subcommand (e.g. `pipe` and `pipeline` for `glab ci`) |
|
|
94
|
+
|
|
95
|
+
An alias is another name for the same definition, not a copy of it: `glab pipeline view` classifies exactly as `glab ci view` and is reported as `["glab", "ci", "view"]`, while `argv` keeps the word that was typed. Declare aliases only for names the tool itself accepts (`glab ci --help` lists `pipe` and `pipeline`), and never a name a sibling subcommand already uses — that is rejected when the file loads.
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
subcommands:
|
|
99
|
+
ci:
|
|
100
|
+
aliases: [pipe, pipeline]
|
|
101
|
+
subcommands:
|
|
102
|
+
list:
|
|
103
|
+
classification: READONLY
|
|
104
|
+
```
|
|
105
|
+
|
|
87
106
|
#### Option fields
|
|
88
107
|
|
|
89
108
|
| Field | Type | Description |
|
|
@@ -330,20 +349,43 @@ options:
|
|
|
330
349
|
-c: {takes_value: true}
|
|
331
350
|
```
|
|
332
351
|
|
|
352
|
+
### `args_are_expression`
|
|
353
|
+
|
|
354
|
+
Every argument is joined with single spaces and the result is parsed as a complete shell expression. This is what
|
|
355
|
+
`eval` does: it concatenates its arguments and runs the concatenation as shell source, so `eval ls -la` and
|
|
356
|
+
`eval "ls -la"` are the same command. Unlike `flag_value_is_expression`, no flag selects the expression -- every
|
|
357
|
+
token after the binary is part of it, so a command using this mode must not declare options of its own.
|
|
358
|
+
|
|
359
|
+
**eval:** `eval "git push --force"` -- the string `git push --force` is parsed as a full expression, producing one
|
|
360
|
+
inner command.
|
|
361
|
+
|
|
362
|
+
```yaml
|
|
363
|
+
command: eval
|
|
364
|
+
classification: DANGEROUS
|
|
365
|
+
strict: false
|
|
366
|
+
delegates_to:
|
|
367
|
+
mode: args_are_expression
|
|
368
|
+
min_classification: DANGEROUS # eval stays DANGEROUS even when the inner is READONLY
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
The `min_classification` floor is not optional here. Command-level delegation erases the wrapper's own base
|
|
372
|
+
classification once it resolves an inner command, so without the floor `eval "ls"` would classify READONLY.
|
|
373
|
+
|
|
333
374
|
### Delegation fields
|
|
334
375
|
|
|
335
376
|
| Field | Type | Applies to | Description |
|
|
336
377
|
|-------|------|-----------|-------------|
|
|
337
|
-
| `mode` | enum | all | One of the
|
|
378
|
+
| `mode` | enum | all | One of the modes above |
|
|
338
379
|
| `separator` | string | `after_separator` | Token that separates wrapper args from inner args |
|
|
339
380
|
| `terminator` | string | `terminated_argv` | Token that ends the inner argv |
|
|
340
381
|
| `flag` | string | `flag_value_is_expression` | Which flag's value to parse as an expression |
|
|
382
|
+
| `skip_leading_positionals` | integer | `rest_are_argv` | Positional tokens belonging to the wrapper itself before the inner command starts (e.g. `timeout DURATION COMMAND...` uses `1`) |
|
|
341
383
|
| `strip_assignments` | boolean | `rest_are_argv` | Strip leading `KEY=VALUE` tokens before inner command |
|
|
342
384
|
| `min_classification` | enum | all | Floor classification for the inner command |
|
|
343
385
|
|
|
344
386
|
### `min_classification`
|
|
345
387
|
|
|
346
|
-
Forces the inner command to be classified at least at the given level. sudo uses this to ensure that even `sudo ls` is at least EXTERNAL_EFFECTS -- because running anything under elevated privileges is not a no-op.
|
|
388
|
+
Forces the inner command to be classified at least at the given level. sudo uses this to ensure that even `sudo ls` is at least EXTERNAL_EFFECTS -- because running anything under elevated privileges is not a no-op. `eval` and `exec` use it the same way, with a `DANGEROUS` floor.
|
|
347
389
|
|
|
348
390
|
## 7. Special Cases
|
|
349
391
|
|
|
@@ -362,9 +404,14 @@ These cannot be modeled as database entries because they are shell builtins with
|
|
|
362
404
|
| Builtin | Classification | Reason |
|
|
363
405
|
|---------|---------------|--------|
|
|
364
406
|
| `cd`, `pushd`, `popd` | READONLY | Directory navigation only |
|
|
365
|
-
| `
|
|
366
|
-
| `source`, `.` | DANGEROUS | Executes an external script |
|
|
367
|
-
|
|
407
|
+
| `[`, `[[`, `test` | READONLY | Condition evaluation, no side effects |
|
|
408
|
+
| `source`, `.` | DANGEROUS | Executes an external script, whose contents are never visible |
|
|
409
|
+
|
|
410
|
+
`eval` and `exec` used to be in this list. They are database entries now -- `commands/eval.yaml`
|
|
411
|
+
delegates via `args_are_expression`, `commands/exec.yaml` via `rest_are_argv`, both with
|
|
412
|
+
`min_classification: DANGEROUS`. Modelling them in YAML exposes the inner command instead of
|
|
413
|
+
hiding it, and the floor is what keeps them DANGEROUS even when the inner command is READONLY.
|
|
414
|
+
Reach for a hardcoded builtin only when the command's argument is genuinely not a command line.
|
|
368
415
|
|
|
369
416
|
### Path-qualified commands
|
|
370
417
|
|