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.
Files changed (197) hide show
  1. {bash_classify-0.4.2 → bash_classify-0.6.0}/.claude-plugin/marketplace.json +1 -1
  2. {bash_classify-0.4.2 → bash_classify-0.6.0}/PKG-INFO +9 -5
  3. {bash_classify-0.4.2 → bash_classify-0.6.0}/README.md +8 -4
  4. {bash_classify-0.4.2 → bash_classify-0.6.0}/SPEC.md +19 -1
  5. {bash_classify-0.4.2 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
  6. {bash_classify-0.4.2 → bash_classify-0.6.0}/docs/classification-guidance.md +9 -1
  7. {bash_classify-0.4.2 → bash_classify-0.6.0}/schemas/command.schema.json +26 -0
  8. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/classifier.py +47 -2
  9. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/cli.py +10 -0
  10. bash_classify-0.6.0/src/bash_classify/commands/gws.yaml +211 -0
  11. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tee.yaml +1 -0
  12. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/database.py +24 -0
  13. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/matcher.py +11 -0
  14. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/models.py +5 -0
  15. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_classifier.py +113 -0
  16. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_cli_serialization.py +99 -0
  17. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_database.py +39 -0
  18. {bash_classify-0.4.2 → bash_classify-0.6.0}/.claude/rules/command-database.md +0 -0
  19. {bash_classify-0.4.2 → bash_classify-0.6.0}/.editorconfig +0 -0
  20. {bash_classify-0.4.2 → bash_classify-0.6.0}/.github/workflows/ci.yml +0 -0
  21. {bash_classify-0.4.2 → bash_classify-0.6.0}/.github/workflows/publish.yml +0 -0
  22. {bash_classify-0.4.2 → bash_classify-0.6.0}/.gitignore +0 -0
  23. {bash_classify-0.4.2 → bash_classify-0.6.0}/.python-version +0 -0
  24. {bash_classify-0.4.2 → bash_classify-0.6.0}/CLAUDE.md +0 -0
  25. {bash_classify-0.4.2 → bash_classify-0.6.0}/LICENSE +0 -0
  26. {bash_classify-0.4.2 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh +0 -0
  27. {bash_classify-0.4.2 → bash_classify-0.6.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
  28. {bash_classify-0.4.2 → bash_classify-0.6.0}/pyproject.toml +0 -0
  29. {bash_classify-0.4.2 → bash_classify-0.6.0}/ruff.toml +0 -0
  30. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/__init__.py +0 -0
  31. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/__main__.py +0 -0
  32. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
  33. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/apt.yaml +0 -0
  34. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/awk.yaml +0 -0
  35. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/basename.yaml +0 -0
  36. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
  37. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/bash.yaml +0 -0
  38. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/black.yaml +0 -0
  39. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/brew.yaml +0 -0
  40. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/builtin.yaml +0 -0
  41. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cargo.yaml +0 -0
  42. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cat.yaml +0 -0
  43. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/chgrp.yaml +0 -0
  44. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/chmod.yaml +0 -0
  45. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/chown.yaml +0 -0
  46. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/column.yaml +0 -0
  47. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/comm.yaml +0 -0
  48. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/command.yaml +0 -0
  49. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cp.yaml +0 -0
  50. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/curl.yaml +0 -0
  51. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/cut.yaml +0 -0
  52. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/date.yaml +0 -0
  53. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/df.yaml +0 -0
  54. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/diff.yaml +0 -0
  55. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/dig.yaml +0 -0
  56. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/dirname.yaml +0 -0
  57. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/docker.yaml +0 -0
  58. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/du.yaml +0 -0
  59. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/echo.yaml +0 -0
  60. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/env.yaml +0 -0
  61. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/eslint.yaml +0 -0
  62. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/expand.yaml +0 -0
  63. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/false.yaml +0 -0
  64. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/file.yaml +0 -0
  65. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/find.yaml +0 -0
  66. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/fmt.yaml +0 -0
  67. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/fold.yaml +0 -0
  68. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/free.yaml +0 -0
  69. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gawk.yaml +0 -0
  70. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gcloud.yaml +0 -0
  71. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gh.yaml +0 -0
  72. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/git.yaml +0 -0
  73. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
  74. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/glab.yaml +0 -0
  75. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/go.yaml +0 -0
  76. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gradle.yaml +0 -0
  77. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gradlew.yaml +0 -0
  78. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/grep.yaml +0 -0
  79. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/groups.yaml +0 -0
  80. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gunzip.yaml +0 -0
  81. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/gzip.yaml +0 -0
  82. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/hadolint.yaml +0 -0
  83. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/head.yaml +0 -0
  84. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/helm.yaml +0 -0
  85. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/hostname.yaml +0 -0
  86. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/id.yaml +0 -0
  87. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/install.yaml +0 -0
  88. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/isort.yaml +0 -0
  89. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/java.yaml +0 -0
  90. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/javac.yaml +0 -0
  91. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/join.yaml +0 -0
  92. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/journalctl.yaml +0 -0
  93. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/jq.yaml +0 -0
  94. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/kill.yaml +0 -0
  95. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/killall.yaml +0 -0
  96. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/kubectl.yaml +0 -0
  97. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/less.yaml +0 -0
  98. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ln.yaml +0 -0
  99. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ls.yaml +0 -0
  100. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/make.yaml +0 -0
  101. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/md5sum.yaml +0 -0
  102. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mkdir.yaml +0 -0
  103. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/more.yaml +0 -0
  104. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mv.yaml +0 -0
  105. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mvn.yaml +0 -0
  106. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mvnw.yaml +0 -0
  107. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/mypy.yaml +0 -0
  108. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/netstat.yaml +0 -0
  109. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nice.yaml +0 -0
  110. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nl.yaml +0 -0
  111. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/node.yaml +0 -0
  112. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nohup.yaml +0 -0
  113. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/npm.yaml +0 -0
  114. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/npx.yaml +0 -0
  115. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/nslookup.yaml +0 -0
  116. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/paste.yaml +0 -0
  117. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ping.yaml +0 -0
  118. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pip.yaml +0 -0
  119. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pip3.yaml +0 -0
  120. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pipx.yaml +0 -0
  121. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pkill.yaml +0 -0
  122. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/poetry.yaml +0 -0
  123. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/prettier.yaml +0 -0
  124. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/printenv.yaml +0 -0
  125. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/printf.yaml +0 -0
  126. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ps.yaml +0 -0
  127. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pup.yaml +0 -0
  128. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pwd.yaml +0 -0
  129. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/pytest.yaml +0 -0
  130. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/python.yaml +0 -0
  131. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/python3.yaml +0 -0
  132. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/read.yaml +0 -0
  133. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/readlink.yaml +0 -0
  134. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/realpath.yaml +0 -0
  135. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rev.yaml +0 -0
  136. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rg.yaml +0 -0
  137. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rm.yaml +0 -0
  138. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/rsync.yaml +0 -0
  139. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ruff.yaml +0 -0
  140. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/scp.yaml +0 -0
  141. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sed.yaml +0 -0
  142. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/seq.yaml +0 -0
  143. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sh.yaml +0 -0
  144. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
  145. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
  146. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
  147. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sort.yaml +0 -0
  148. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ss.yaml +0 -0
  149. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/ssh.yaml +0 -0
  150. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/stat.yaml +0 -0
  151. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/strace.yaml +0 -0
  152. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/sudo.yaml +0 -0
  153. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/systemctl.yaml +0 -0
  154. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tac.yaml +0 -0
  155. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tail.yaml +0 -0
  156. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tar.yaml +0 -0
  157. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/terraform.yaml +0 -0
  158. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/test.yaml +0 -0
  159. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/time.yaml +0 -0
  160. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/timeout.yaml +0 -0
  161. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/top.yaml +0 -0
  162. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/touch.yaml +0 -0
  163. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tr.yaml +0 -0
  164. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/traceroute.yaml +0 -0
  165. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/true.yaml +0 -0
  166. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/tsc.yaml +0 -0
  167. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/type.yaml +0 -0
  168. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uname.yaml +0 -0
  169. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/unexpand.yaml +0 -0
  170. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uniq.yaml +0 -0
  171. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/unzip.yaml +0 -0
  172. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uptime.yaml +0 -0
  173. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/uv.yaml +0 -0
  174. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/wc.yaml +0 -0
  175. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/wget.yaml +0 -0
  176. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/whereis.yaml +0 -0
  177. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/which.yaml +0 -0
  178. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/whoami.yaml +0 -0
  179. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/xargs.yaml +0 -0
  180. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/xmllint.yaml +0 -0
  181. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/yarn.yaml +0 -0
  182. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/yes.yaml +0 -0
  183. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/yq.yaml +0 -0
  184. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/zip.yaml +0 -0
  185. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/commands/zsh.yaml +0 -0
  186. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/parser.py +0 -0
  187. {bash_classify-0.4.2 → bash_classify-0.6.0}/src/bash_classify/py.typed +0 -0
  188. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/__init__.py +0 -0
  189. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/conftest.py +0 -0
  190. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_cli.py +0 -0
  191. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_matcher.py +0 -0
  192. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_models.py +0 -0
  193. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_parser.py +0 -0
  194. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_real_world.py +0 -0
  195. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_schema.py +0 -0
  196. {bash_classify-0.4.2 → bash_classify-0.6.0}/tests/test_smoke.py +0 -0
  197. {bash_classify-0.4.2 → bash_classify-0.6.0}/uv.lock +0 -0
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "bash-classify-hook",
10
10
  "description": "Auto-allows low-risk bash commands in Claude Code using bash-classify",
11
- "version": "0.4.2",
11
+ "version": "0.6.0",
12
12
  "author": {
13
13
  "name": "Filip Procházka",
14
14
  "url": "https://github.com/fprochazka"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bash-classify
3
- Version: 0.4.2
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 120+ 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.
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
- The classification database includes 120+ command definitions covering common Unix utilities, package managers, container tools, cloud CLIs, and more.
89
+ bash-classify loads command definitions from two locations:
90
90
 
91
- - See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands
92
- - YAML definitions are validated against a JSON Schema for IDE autocomplete and CI checks
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 120+ 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.
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
- The classification database includes 120+ command definitions covering common Unix utilities, package managers, container tools, cloud CLIs, and more.
60
+ bash-classify loads command definitions from two locations:
61
61
 
62
- - See [docs/classification-guidance.md](docs/classification-guidance.md) for how to add new commands
63
- - YAML definitions are validated against a JSON Schema for IDE autocomplete and CI checks
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bash-classify-hook",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "Auto-allows low-risk bash commands in Claude Code using bash-classify",
5
5
  "author": {
6
6
  "name": "Filip Prochazka"
@@ -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. Common Patterns
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
- # Elevate risk to at least MEDIUM for output redirects
118
- result.risk = Risk.max_severity(result.risk, Risk.MEDIUM)
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}
@@ -2,6 +2,7 @@
2
2
  command: tee
3
3
  description: "Read from stdin and write to stdout and files"
4
4
  classification: LOCAL_EFFECTS
5
+ risk: LOW
5
6
  strict: false
6
7
  global_options:
7
8
  --help: {overrides: READONLY}
@@ -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]