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