bash-classify 0.4.1__tar.gz → 0.5.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 (196) hide show
  1. {bash_classify-0.4.1 → bash_classify-0.5.0}/.claude-plugin/marketplace.json +1 -1
  2. {bash_classify-0.4.1 → bash_classify-0.5.0}/PKG-INFO +1 -1
  3. {bash_classify-0.4.1 → bash_classify-0.5.0}/SPEC.md +19 -1
  4. {bash_classify-0.4.1 → bash_classify-0.5.0}/coding-agent-plugins/claude-code/.claude-plugin/plugin.json +1 -1
  5. {bash_classify-0.4.1 → bash_classify-0.5.0}/docs/classification-guidance.md +9 -1
  6. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/classifier.py +47 -2
  7. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/cli.py +10 -0
  8. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/pup.yaml +38 -0
  9. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/tee.yaml +1 -0
  10. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/models.py +4 -0
  11. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_classifier.py +56 -0
  12. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_cli_serialization.py +99 -0
  13. {bash_classify-0.4.1 → bash_classify-0.5.0}/.claude/rules/command-database.md +0 -0
  14. {bash_classify-0.4.1 → bash_classify-0.5.0}/.editorconfig +0 -0
  15. {bash_classify-0.4.1 → bash_classify-0.5.0}/.github/workflows/ci.yml +0 -0
  16. {bash_classify-0.4.1 → bash_classify-0.5.0}/.github/workflows/publish.yml +0 -0
  17. {bash_classify-0.4.1 → bash_classify-0.5.0}/.gitignore +0 -0
  18. {bash_classify-0.4.1 → bash_classify-0.5.0}/.python-version +0 -0
  19. {bash_classify-0.4.1 → bash_classify-0.5.0}/CLAUDE.md +0 -0
  20. {bash_classify-0.4.1 → bash_classify-0.5.0}/LICENSE +0 -0
  21. {bash_classify-0.4.1 → bash_classify-0.5.0}/README.md +0 -0
  22. {bash_classify-0.4.1 → bash_classify-0.5.0}/coding-agent-plugins/claude-code/hooks/classify-bash.sh +0 -0
  23. {bash_classify-0.4.1 → bash_classify-0.5.0}/coding-agent-plugins/claude-code/hooks/hooks.json +0 -0
  24. {bash_classify-0.4.1 → bash_classify-0.5.0}/pyproject.toml +0 -0
  25. {bash_classify-0.4.1 → bash_classify-0.5.0}/ruff.toml +0 -0
  26. {bash_classify-0.4.1 → bash_classify-0.5.0}/schemas/command.schema.json +0 -0
  27. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/__init__.py +0 -0
  28. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/__main__.py +0 -0
  29. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/agent-browser.yaml +0 -0
  30. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/apt.yaml +0 -0
  31. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/awk.yaml +0 -0
  32. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/basename.yaml +0 -0
  33. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/bash-classify.yaml +0 -0
  34. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/bash.yaml +0 -0
  35. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/black.yaml +0 -0
  36. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/brew.yaml +0 -0
  37. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/builtin.yaml +0 -0
  38. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/cargo.yaml +0 -0
  39. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/cat.yaml +0 -0
  40. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/chgrp.yaml +0 -0
  41. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/chmod.yaml +0 -0
  42. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/chown.yaml +0 -0
  43. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/column.yaml +0 -0
  44. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/comm.yaml +0 -0
  45. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/command.yaml +0 -0
  46. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/cp.yaml +0 -0
  47. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/curl.yaml +0 -0
  48. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/cut.yaml +0 -0
  49. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/date.yaml +0 -0
  50. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/df.yaml +0 -0
  51. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/diff.yaml +0 -0
  52. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/dig.yaml +0 -0
  53. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/dirname.yaml +0 -0
  54. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/docker.yaml +0 -0
  55. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/du.yaml +0 -0
  56. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/echo.yaml +0 -0
  57. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/env.yaml +0 -0
  58. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/eslint.yaml +0 -0
  59. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/expand.yaml +0 -0
  60. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/false.yaml +0 -0
  61. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/file.yaml +0 -0
  62. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/find.yaml +0 -0
  63. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/fmt.yaml +0 -0
  64. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/fold.yaml +0 -0
  65. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/free.yaml +0 -0
  66. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/gawk.yaml +0 -0
  67. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/gcloud.yaml +0 -0
  68. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/gh.yaml +0 -0
  69. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/git.yaml +0 -0
  70. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/glab-discussion.yaml +0 -0
  71. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/glab.yaml +0 -0
  72. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/go.yaml +0 -0
  73. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/gradle.yaml +0 -0
  74. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/gradlew.yaml +0 -0
  75. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/grep.yaml +0 -0
  76. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/groups.yaml +0 -0
  77. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/gunzip.yaml +0 -0
  78. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/gzip.yaml +0 -0
  79. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/hadolint.yaml +0 -0
  80. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/head.yaml +0 -0
  81. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/helm.yaml +0 -0
  82. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/hostname.yaml +0 -0
  83. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/id.yaml +0 -0
  84. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/install.yaml +0 -0
  85. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/isort.yaml +0 -0
  86. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/java.yaml +0 -0
  87. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/javac.yaml +0 -0
  88. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/join.yaml +0 -0
  89. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/journalctl.yaml +0 -0
  90. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/jq.yaml +0 -0
  91. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/kill.yaml +0 -0
  92. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/killall.yaml +0 -0
  93. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/kubectl.yaml +0 -0
  94. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/less.yaml +0 -0
  95. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/ln.yaml +0 -0
  96. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/ls.yaml +0 -0
  97. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/make.yaml +0 -0
  98. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/md5sum.yaml +0 -0
  99. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/mkdir.yaml +0 -0
  100. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/more.yaml +0 -0
  101. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/mv.yaml +0 -0
  102. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/mvn.yaml +0 -0
  103. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/mvnw.yaml +0 -0
  104. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/mypy.yaml +0 -0
  105. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/netstat.yaml +0 -0
  106. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/nice.yaml +0 -0
  107. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/nl.yaml +0 -0
  108. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/node.yaml +0 -0
  109. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/nohup.yaml +0 -0
  110. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/npm.yaml +0 -0
  111. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/npx.yaml +0 -0
  112. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/nslookup.yaml +0 -0
  113. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/paste.yaml +0 -0
  114. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/ping.yaml +0 -0
  115. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/pip.yaml +0 -0
  116. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/pip3.yaml +0 -0
  117. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/pipx.yaml +0 -0
  118. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/pkill.yaml +0 -0
  119. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/poetry.yaml +0 -0
  120. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/prettier.yaml +0 -0
  121. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/printenv.yaml +0 -0
  122. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/printf.yaml +0 -0
  123. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/ps.yaml +0 -0
  124. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/pwd.yaml +0 -0
  125. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/pytest.yaml +0 -0
  126. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/python.yaml +0 -0
  127. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/python3.yaml +0 -0
  128. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/read.yaml +0 -0
  129. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/readlink.yaml +0 -0
  130. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/realpath.yaml +0 -0
  131. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/rev.yaml +0 -0
  132. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/rg.yaml +0 -0
  133. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/rm.yaml +0 -0
  134. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/rsync.yaml +0 -0
  135. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/ruff.yaml +0 -0
  136. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/scp.yaml +0 -0
  137. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/sed.yaml +0 -0
  138. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/seq.yaml +0 -0
  139. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/sh.yaml +0 -0
  140. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/sha1sum.yaml +0 -0
  141. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/sha256sum.yaml +0 -0
  142. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/shellcheck.yaml +0 -0
  143. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/sort.yaml +0 -0
  144. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/ss.yaml +0 -0
  145. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/ssh.yaml +0 -0
  146. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/stat.yaml +0 -0
  147. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/strace.yaml +0 -0
  148. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/sudo.yaml +0 -0
  149. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/systemctl.yaml +0 -0
  150. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/tac.yaml +0 -0
  151. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/tail.yaml +0 -0
  152. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/tar.yaml +0 -0
  153. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/terraform.yaml +0 -0
  154. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/test.yaml +0 -0
  155. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/time.yaml +0 -0
  156. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/timeout.yaml +0 -0
  157. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/top.yaml +0 -0
  158. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/touch.yaml +0 -0
  159. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/tr.yaml +0 -0
  160. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/traceroute.yaml +0 -0
  161. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/true.yaml +0 -0
  162. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/tsc.yaml +0 -0
  163. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/type.yaml +0 -0
  164. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/uname.yaml +0 -0
  165. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/unexpand.yaml +0 -0
  166. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/uniq.yaml +0 -0
  167. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/unzip.yaml +0 -0
  168. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/uptime.yaml +0 -0
  169. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/uv.yaml +0 -0
  170. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/wc.yaml +0 -0
  171. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/wget.yaml +0 -0
  172. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/whereis.yaml +0 -0
  173. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/which.yaml +0 -0
  174. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/whoami.yaml +0 -0
  175. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/xargs.yaml +0 -0
  176. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/xmllint.yaml +0 -0
  177. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/yarn.yaml +0 -0
  178. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/yes.yaml +0 -0
  179. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/yq.yaml +0 -0
  180. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/zip.yaml +0 -0
  181. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/commands/zsh.yaml +0 -0
  182. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/database.py +0 -0
  183. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/matcher.py +0 -0
  184. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/parser.py +0 -0
  185. {bash_classify-0.4.1 → bash_classify-0.5.0}/src/bash_classify/py.typed +0 -0
  186. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/__init__.py +0 -0
  187. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/conftest.py +0 -0
  188. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_cli.py +0 -0
  189. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_database.py +0 -0
  190. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_matcher.py +0 -0
  191. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_models.py +0 -0
  192. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_parser.py +0 -0
  193. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_real_world.py +0 -0
  194. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_schema.py +0 -0
  195. {bash_classify-0.4.1 → bash_classify-0.5.0}/tests/test_smoke.py +0 -0
  196. {bash_classify-0.4.1 → bash_classify-0.5.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.1",
11
+ "version": "0.5.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.1
3
+ Version: 0.5.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
@@ -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.1",
3
+ "version": "0.5.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
  |---------|---------|---------------|
@@ -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
 
@@ -13,8 +13,10 @@ subcommands:
13
13
 
14
14
  apm:
15
15
  classification: READONLY
16
+ strict: false
16
17
  audit-logs:
17
18
  classification: READONLY
19
+ strict: false
18
20
  auth:
19
21
  classification: LOCAL_EFFECTS
20
22
  strict: false
@@ -33,76 +35,112 @@ subcommands:
33
35
  classification: LOCAL_EFFECTS
34
36
  cloud:
35
37
  classification: READONLY
38
+ strict: false
36
39
  containers:
37
40
  classification: READONLY
41
+ strict: false
38
42
  cost:
39
43
  classification: READONLY
44
+ strict: false
40
45
  dbm:
41
46
  classification: READONLY
47
+ strict: false
42
48
  ddsql:
43
49
  classification: READONLY
50
+ strict: false
44
51
  error-tracking:
45
52
  classification: READONLY
53
+ strict: false
46
54
  events:
47
55
  classification: READONLY
56
+ strict: false
48
57
  fleet:
49
58
  classification: READONLY
59
+ strict: false
50
60
  infrastructure:
51
61
  classification: READONLY
62
+ strict: false
52
63
  llm-obs:
53
64
  classification: READONLY
65
+ strict: false
54
66
  logs:
55
67
  classification: READONLY
68
+ strict: false
56
69
  on-call:
57
70
  classification: READONLY
71
+ strict: false
58
72
  organizations:
59
73
  classification: READONLY
74
+ strict: false
60
75
  rum:
61
76
  classification: READONLY
77
+ strict: false
62
78
  scorecards:
63
79
  classification: READONLY
80
+ strict: false
64
81
  service-catalog:
65
82
  classification: READONLY
83
+ strict: false
66
84
  static-analysis:
67
85
  classification: READONLY
86
+ strict: false
68
87
  status-pages:
69
88
  classification: READONLY
89
+ strict: false
70
90
  symdb:
71
91
  classification: READONLY
92
+ strict: false
72
93
  synthetics:
73
94
  classification: READONLY
95
+ strict: false
74
96
  traces:
75
97
  classification: READONLY
98
+ strict: false
76
99
  usage:
77
100
  classification: READONLY
101
+ strict: false
78
102
  users:
79
103
  classification: READONLY
104
+ strict: false
80
105
  version:
81
106
  classification: READONLY
107
+ strict: false
82
108
  status:
83
109
  classification: READONLY
110
+ strict: false
84
111
  completions:
85
112
  classification: READONLY
113
+ strict: false
86
114
  test-optimization:
87
115
  classification: READONLY
116
+ strict: false
88
117
  code-coverage:
89
118
  classification: READONLY
119
+ strict: false
90
120
  cicd:
91
121
  classification: READONLY
122
+ strict: false
92
123
  product-analytics:
93
124
  classification: READONLY
125
+ strict: false
94
126
  data-governance:
95
127
  classification: READONLY
128
+ strict: false
96
129
  feature-flags:
97
130
  classification: READONLY
131
+ strict: false
98
132
  bits:
99
133
  classification: READONLY
134
+ strict: false
100
135
  hamr:
101
136
  classification: READONLY
137
+ strict: false
102
138
  misc:
103
139
  classification: READONLY
140
+ strict: false
104
141
  agent:
105
142
  classification: READONLY
143
+ strict: false
106
144
 
107
145
 
108
146
  monitors:
@@ -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}
@@ -177,6 +177,8 @@ class CommandResult:
177
177
  classification_reason: str | None = None
178
178
  overriding_option: str | None = None
179
179
  directories: list[str] | None = None
180
+ write_paths: list[str] | None = None
181
+ read_paths: list[str] | None = None
180
182
 
181
183
 
182
184
  @dataclass
@@ -187,6 +189,8 @@ class ExpressionResult:
187
189
  classification: Classification
188
190
  risk: Risk
189
191
  directories: list[str]
192
+ write_paths: list[str]
193
+ read_paths: list[str]
190
194
  commands: list[CommandResult]
191
195
  redirects: list[Redirect]
192
196
  parse_warnings: list[str]
@@ -631,3 +631,59 @@ class TestRiskExpressionAggregation:
631
631
  result = classify_expression("if then fi", database=database)
632
632
  assert result.classification == Classification.UNKNOWN
633
633
  assert result.risk == Risk.HIGH
634
+
635
+
636
+ class TestFilePathDetection:
637
+ """Tests for write_paths/read_paths detection and temp path risk lowering."""
638
+
639
+ def test_write_to_tmp_stays_low_risk(self, database: dict[str, CommandDef]) -> None:
640
+ result = classify_expression("cat > /tmp/foo.txt", database=database)
641
+ assert result.classification == Classification.LOCAL_EFFECTS
642
+ assert result.risk == Risk.LOW
643
+ assert result.write_paths == ["/tmp/foo.txt"]
644
+ cmd = result.commands[0]
645
+ assert cmd.write_paths == ["/tmp/foo.txt"]
646
+
647
+ def test_write_to_var_tmp_stays_low_risk(self, database: dict[str, CommandDef]) -> None:
648
+ result = classify_expression("cat > /var/tmp/foo.txt", database=database)
649
+ assert result.classification == Classification.LOCAL_EFFECTS
650
+ assert result.risk == Risk.LOW
651
+ assert result.write_paths == ["/var/tmp/foo.txt"]
652
+
653
+ def test_write_to_home_elevates_risk(self, database: dict[str, CommandDef]) -> None:
654
+ result = classify_expression("cat > /home/user/foo.txt", database=database)
655
+ assert result.classification == Classification.LOCAL_EFFECTS
656
+ assert result.risk == Risk.MEDIUM
657
+ assert result.write_paths == ["/home/user/foo.txt"]
658
+
659
+ def test_mixed_tmp_and_home_elevates_risk(self, database: dict[str, CommandDef]) -> None:
660
+ # Two redirects: one temp, one not -- should elevate
661
+ result = classify_expression("echo hello > /tmp/a.txt > /home/user/b.txt", database=database)
662
+ assert result.risk == Risk.MEDIUM
663
+
664
+ def test_read_redirect_detected(self, database: dict[str, CommandDef]) -> None:
665
+ result = classify_expression("cat < /home/user/data.txt", database=database)
666
+ assert result.classification == Classification.READONLY
667
+ assert result.risk == Risk.LOW
668
+ assert result.read_paths == ["/home/user/data.txt"]
669
+ assert result.write_paths == []
670
+
671
+ def test_read_and_write_redirect(self, database: dict[str, CommandDef]) -> None:
672
+ result = classify_expression("sort < /home/user/hosts > /tmp/sorted.txt", database=database)
673
+ assert result.classification == Classification.LOCAL_EFFECTS
674
+ assert result.risk == Risk.LOW
675
+ assert result.write_paths == ["/tmp/sorted.txt"]
676
+ assert result.read_paths == ["/home/user/hosts"]
677
+
678
+ def test_dev_null_excluded_from_write_paths(self, database: dict[str, CommandDef]) -> None:
679
+ result = classify_expression("cat > /dev/null", database=database)
680
+ assert result.write_paths == []
681
+
682
+ def test_heredoc_not_in_read_paths(self, database: dict[str, CommandDef]) -> None:
683
+ result = classify_expression("cat << EOF\nhello\nEOF", database=database)
684
+ assert result.read_paths == []
685
+
686
+ def test_expression_level_aggregation(self, database: dict[str, CommandDef]) -> None:
687
+ result = classify_expression("cat < /home/user/data.txt | tee /tmp/out.txt > /tmp/log.txt", database=database)
688
+ assert "/home/user/data.txt" in result.read_paths
689
+ assert "/tmp/log.txt" in result.write_paths
@@ -129,6 +129,8 @@ class TestResultToDict:
129
129
  classification=Classification.EXTERNAL_EFFECTS,
130
130
  risk=Risk.MEDIUM,
131
131
  directories=[],
132
+ write_paths=[],
133
+ read_paths=[],
132
134
  commands=[cmd],
133
135
  redirects=[redirect],
134
136
  parse_warnings=["some warning"],
@@ -155,6 +157,8 @@ class TestResultToDict:
155
157
  classification=Classification.READONLY,
156
158
  risk=Risk.LOW,
157
159
  directories=[],
160
+ write_paths=[],
161
+ read_paths=[],
158
162
  commands=[cmd],
159
163
  redirects=[],
160
164
  parse_warnings=[],
@@ -238,6 +242,8 @@ class TestRiskSerialization:
238
242
  classification=Classification.READONLY,
239
243
  risk=Risk.LOW,
240
244
  directories=[],
245
+ write_paths=[],
246
+ read_paths=[],
241
247
  commands=[cmd],
242
248
  redirects=[],
243
249
  parse_warnings=[],
@@ -259,3 +265,96 @@ class TestRiskSerialization:
259
265
  d = _command_to_dict(result)
260
266
  assert d["risk"] == risk.value
261
267
  assert isinstance(d["risk"], str)
268
+
269
+
270
+ class TestWriteReadPaths:
271
+ def test_write_paths_in_command_dict(self) -> None:
272
+ result = CommandResult(
273
+ command=["cat"],
274
+ argv=["cat"],
275
+ classification=Classification.LOCAL_EFFECTS,
276
+ risk=Risk.LOW,
277
+ matched_rule="cat",
278
+ inner_commands=[],
279
+ write_paths=["/tmp/foo.txt"],
280
+ )
281
+ d = _command_to_dict(result)
282
+ assert d["write_paths"] == ["/tmp/foo.txt"]
283
+ assert "read_paths" not in d
284
+
285
+ def test_read_paths_in_command_dict(self) -> None:
286
+ result = CommandResult(
287
+ command=["cat"],
288
+ argv=["cat"],
289
+ classification=Classification.READONLY,
290
+ risk=Risk.LOW,
291
+ matched_rule="cat",
292
+ inner_commands=[],
293
+ read_paths=["/etc/hosts"],
294
+ )
295
+ d = _command_to_dict(result)
296
+ assert d["read_paths"] == ["/etc/hosts"]
297
+ assert "write_paths" not in d
298
+
299
+ def test_paths_omitted_when_empty(self) -> None:
300
+ result = CommandResult(
301
+ command=["ls"],
302
+ argv=["ls"],
303
+ classification=Classification.READONLY,
304
+ risk=Risk.LOW,
305
+ matched_rule="ls",
306
+ inner_commands=[],
307
+ )
308
+ d = _command_to_dict(result)
309
+ assert "write_paths" not in d
310
+ assert "read_paths" not in d
311
+
312
+ def test_expression_write_read_paths(self) -> None:
313
+ cmd = CommandResult(
314
+ command=["cat"],
315
+ argv=["cat"],
316
+ classification=Classification.LOCAL_EFFECTS,
317
+ risk=Risk.LOW,
318
+ matched_rule="cat",
319
+ inner_commands=[],
320
+ write_paths=["/tmp/out.txt"],
321
+ read_paths=["/etc/hosts"],
322
+ )
323
+ result = ExpressionResult(
324
+ expression="cat < /etc/hosts > /tmp/out.txt",
325
+ classification=Classification.LOCAL_EFFECTS,
326
+ risk=Risk.LOW,
327
+ directories=[],
328
+ write_paths=["/tmp/out.txt"],
329
+ read_paths=["/etc/hosts"],
330
+ commands=[cmd],
331
+ redirects=[],
332
+ parse_warnings=[],
333
+ )
334
+ d = _result_to_dict(result)
335
+ assert d["write_paths"] == ["/tmp/out.txt"]
336
+ assert d["read_paths"] == ["/etc/hosts"]
337
+
338
+ def test_expression_paths_omitted_when_empty(self) -> None:
339
+ cmd = CommandResult(
340
+ command=["ls"],
341
+ argv=["ls"],
342
+ classification=Classification.READONLY,
343
+ risk=Risk.LOW,
344
+ matched_rule="ls",
345
+ inner_commands=[],
346
+ )
347
+ result = ExpressionResult(
348
+ expression="ls",
349
+ classification=Classification.READONLY,
350
+ risk=Risk.LOW,
351
+ directories=[],
352
+ write_paths=[],
353
+ read_paths=[],
354
+ commands=[cmd],
355
+ redirects=[],
356
+ parse_warnings=[],
357
+ )
358
+ d = _result_to_dict(result)
359
+ assert "write_paths" not in d
360
+ assert "read_paths" not in d
File without changes
File without changes
File without changes
File without changes
File without changes