claude-dev-env 2.4.0 → 2.7.0

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 (309) hide show
  1. package/CLAUDE.md +26 -59
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  13. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  16. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  18. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  19. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  20. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  21. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  22. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  23. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  24. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  25. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  26. package/_shared/pr-loop/worker-spawn.md +1 -1
  27. package/agents/CLAUDE.md +3 -2
  28. package/agents/caveman.md +0 -1
  29. package/agents/clasp-deployment-orchestrator.md +0 -1
  30. package/agents/clean-coder.md +0 -1
  31. package/agents/code-advisor.md +0 -1
  32. package/agents/code-quality-agent.md +1 -2
  33. package/agents/code-verifier.md +36 -8
  34. package/agents/deep-research.md +0 -1
  35. package/agents/docs-agent.md +0 -1
  36. package/agents/git-commit-crafter.md +0 -1
  37. package/agents/issue-tracker.md +42 -0
  38. package/agents/plan-packet-validator.md +0 -1
  39. package/agents/pr-description-writer.md +0 -1
  40. package/agents/test_agent_frontmatter.py +67 -18
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  42. package/bin/CLAUDE.md +68 -5
  43. package/bin/codex-compat.mjs +104 -0
  44. package/bin/codex-compat.test.mjs +51 -0
  45. package/bin/ever-shipped-skills.mjs +1 -0
  46. package/bin/install-constants.mjs +88 -0
  47. package/bin/install.mjs +1138 -114
  48. package/bin/install.prune.test.mjs +869 -19
  49. package/bin/install.test.mjs +906 -2
  50. package/codex-capability-map.json +13 -0
  51. package/commands/implement.md +1 -1
  52. package/commands/right-size.md +1 -1
  53. package/docs/CLAUDE.md +1 -0
  54. package/docs/CODE_RULES.md +2 -0
  55. package/docs/codex-compatibility.md +25 -0
  56. package/docs/host-pool-health-monitor.md +102 -0
  57. package/docs/nas-ssh-invocation.md +96 -12
  58. package/docs/references/CLAUDE.md +4 -2
  59. package/docs/references/advisor-tool.md +13 -0
  60. package/docs/references/code-review-enforcement.md +35 -0
  61. package/docs/references/team-advisor-skill.md +14 -0
  62. package/hooks/blocking/CLAUDE.md +4 -0
  63. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  64. package/hooks/blocking/code_review_push_gate.py +9 -4
  65. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  66. package/hooks/blocking/config/__init__.py +5 -5
  67. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
  68. package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
  69. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  70. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  71. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  72. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  73. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  74. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  75. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  76. package/hooks/blocking/state_description_blocker.py +1 -1
  77. package/hooks/blocking/stop_dispatcher.py +1 -1
  78. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  79. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  80. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  81. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  82. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  83. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  84. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  85. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  86. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  87. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  88. package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
  89. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  90. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  91. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  92. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  93. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  94. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  95. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  96. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  97. package/hooks/git-hooks/CLAUDE.md +3 -0
  98. package/hooks/git-hooks/conftest.py +30 -0
  99. package/hooks/git-hooks/gate_utils.py +2 -2
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  101. package/hooks/git-hooks/pre_push.py +75 -4
  102. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  103. package/hooks/git-hooks/test_config.py +0 -15
  104. package/hooks/git-hooks/test_gate_utils.py +3 -15
  105. package/hooks/git-hooks/test_pre_commit.py +1 -15
  106. package/hooks/git-hooks/test_pre_push.py +257 -23
  107. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  108. package/hooks/hooks.json +10 -12
  109. package/hooks/hooks_constants/CLAUDE.md +7 -2
  110. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  111. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  112. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  113. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  114. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  115. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  116. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  117. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  118. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  119. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  120. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  121. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  122. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  123. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  124. package/hooks/pyproject.toml +1 -0
  125. package/hooks/validators/CLAUDE.md +1 -0
  126. package/hooks/validators/config/__init__.py +0 -0
  127. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  128. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  129. package/hooks/validators/conftest.py +4 -0
  130. package/hooks/validators/ruff_integration.py +49 -5
  131. package/hooks/validators/run_all_validators.py +206 -9
  132. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +92 -1
  135. package/hooks/validators/test_run_all_validators.py +115 -68
  136. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  137. package/package.json +13 -3
  138. package/rules/CLAUDE.md +17 -22
  139. package/rules/agent-spawn-protocol.md +6 -6
  140. package/rules/anti-corollary-tests.md +1 -1
  141. package/rules/bdd.md +1 -1
  142. package/rules/cleanup-temp-files.md +10 -4
  143. package/rules/code-standards.md +7 -0
  144. package/rules/conservative-action.md +1 -5
  145. package/rules/context7.md +0 -4
  146. package/rules/destructive-commands.md +47 -0
  147. package/rules/doc-inventory-integrity.md +48 -0
  148. package/rules/doc-prose-cuts.md +58 -0
  149. package/rules/docstring-prose-matches-implementation.md +53 -44
  150. package/rules/durable-post-artifacts.md +0 -4
  151. package/rules/eli11-replies.md +31 -0
  152. package/rules/explore-thoroughly.md +4 -4
  153. package/rules/falsify-before-green.md +68 -0
  154. package/rules/file-global-constants.md +1 -1
  155. package/rules/filesystem-search.md +51 -0
  156. package/rules/gh-cli-conventions.md +27 -0
  157. package/rules/git-workflow.md +26 -0
  158. package/rules/hedging-claims.md +9 -0
  159. package/rules/long-horizon-autonomy.md +0 -4
  160. package/rules/measurement-denominators.md +48 -0
  161. package/rules/nas-ssh-invocation.md +23 -5
  162. package/rules/parallel-tools.md +2 -2
  163. package/rules/plain-illustrative-docstrings.md +3 -7
  164. package/rules/plain-language.md +2 -0
  165. package/rules/proof-of-work-pr-comments.md +0 -4
  166. package/rules/re-stage-before-commit.md +2 -0
  167. package/rules/research-mode.md +10 -0
  168. package/rules/shell-invocation.md +21 -0
  169. package/rules/testing.md +4 -0
  170. package/rules/verified-commit-gate-skip.md +3 -27
  171. package/rules/verify-before-asking.md +5 -0
  172. package/rules/windows-filesystem-safe.md +1 -1
  173. package/rules/workers-done-before-complete.md +4 -0
  174. package/scripts/CLAUDE.md +1 -0
  175. package/scripts/Capture-PoolHealth.ps1 +410 -0
  176. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  177. package/scripts/_code_review_test_support.py +404 -0
  178. package/scripts/claude_chain_runner.py +141 -1
  179. package/scripts/codex_capability_bridge.py +171 -0
  180. package/scripts/codex_compat_materializer.py +1087 -0
  181. package/scripts/codex_compat_watcher.py +502 -0
  182. package/scripts/conftest.py +16 -1
  183. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  184. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  185. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  186. package/scripts/invoke_code_review.py +11 -4
  187. package/scripts/resolve_worker_spawn.py +626 -626
  188. package/scripts/spawn_grok_batch.py +672 -672
  189. package/scripts/sync_to_cursor/rules.py +0 -10
  190. package/scripts/test_claude_chain_runner.py +131 -0
  191. package/scripts/test_invoke_code_review.py +85 -908
  192. package/scripts/test_invoke_code_review_chain.py +70 -0
  193. package/scripts/test_invoke_code_review_cli.py +192 -0
  194. package/scripts/test_invoke_code_review_contract.py +256 -0
  195. package/scripts/test_invoke_code_review_git.py +123 -0
  196. package/scripts/test_invoke_code_review_mode.py +99 -0
  197. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  198. package/scripts/tests/test_code_review_constants.py +80 -0
  199. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  200. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  201. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  202. package/scripts/tests/test_sync_to_cursor.py +0 -1
  203. package/skills/CLAUDE.md +2 -0
  204. package/skills/auditing-claude-config/SKILL.md +114 -114
  205. package/skills/autoconverge/SKILL.md +427 -427
  206. package/skills/autoconverge/reference/convergence.md +24 -3
  207. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  208. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  209. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  210. package/skills/autoconverge/workflow/converge.mjs +168 -1
  211. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  212. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  213. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  214. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  215. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  216. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  217. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  218. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  219. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  220. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  221. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  222. package/skills/closeout/SKILL.md +33 -50
  223. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  224. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  225. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  226. package/skills/condensing-instructions/SKILL.md +72 -0
  227. package/skills/copilot-review/SKILL.md +119 -119
  228. package/skills/e-code-review/SKILL.md +52 -0
  229. package/skills/e-code-review/reference/fix.md +54 -0
  230. package/skills/e-code-review/reference/loop.md +43 -0
  231. package/skills/e-code-review/reference/low.md +57 -0
  232. package/skills/e-code-review/reference/medium.md +153 -0
  233. package/skills/e-code-review/reference/xhigh.md +182 -0
  234. package/skills/e-simplify/SKILL.md +97 -0
  235. package/skills/fresh-branch/CLAUDE.md +1 -1
  236. package/skills/fresh-branch/SKILL.md +5 -6
  237. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  238. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  239. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  240. package/skills/issue-tracker/SKILL.md +92 -0
  241. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  242. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  243. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  244. package/skills/orchestrator/SKILL.md +177 -22
  245. package/skills/orchestrator/scripts/status_gate.py +625 -0
  246. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  247. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  248. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  249. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  250. package/skills/orchestrator-refresh/SKILL.md +129 -35
  251. package/skills/plan-to-pr/SKILL.md +155 -0
  252. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  253. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  254. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  255. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  256. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  257. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  258. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  259. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  260. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  261. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  262. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  263. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  264. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  265. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  266. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  267. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  268. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  269. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  270. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  271. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  272. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  273. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  274. package/skills/pr-converge/SKILL.md +472 -469
  275. package/skills/pr-converge/reference/examples.md +3 -3
  276. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  277. package/skills/pr-converge/reference/ground-rules.md +7 -4
  278. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  279. package/skills/pr-converge/reference/per-tick.md +5 -5
  280. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  281. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  282. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  283. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  284. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  285. package/skills/pr-fix-protocol/SKILL.md +1 -1
  286. package/skills/privacy-hygiene/SKILL.md +68 -68
  287. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  288. package/skills/prototype/workflows/promotion.md +1 -1
  289. package/skills/release-notes-html/SKILL.md +164 -0
  290. package/skills/session-log/SKILL.md +1 -1
  291. package/skills/task-build/CLAUDE.md +8 -7
  292. package/skills/task-build/SKILL.md +16 -8
  293. package/skills/task-build/reference/tool-routing.md +19 -0
  294. package/rules/claude-md-orphan-file.md +0 -28
  295. package/rules/cleanup-command-forms.md +0 -23
  296. package/rules/code-reviews.md +0 -11
  297. package/rules/env-var-table-code-drift.md +0 -10
  298. package/rules/gh-body-file.md +0 -5
  299. package/rules/gh-paginate.md +0 -3
  300. package/rules/hook-prose-matches-detector.md +0 -15
  301. package/rules/no-historical-clutter.md +0 -26
  302. package/rules/no-inline-destructive-literals.md +0 -9
  303. package/rules/no-justification-noise.md +0 -61
  304. package/rules/package-inventory-stale-entry.md +0 -25
  305. package/rules/right-sized-engineering.md +0 -28
  306. package/rules/self-contained-docs.md +0 -17
  307. package/rules/shell-invocation-policy.md +0 -5
  308. package/rules/tdd.md +0 -7
  309. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -300,6 +300,15 @@ def test_plain_language_denial_on_multi_edit_of_markdown_file() -> None:
300
300
  _assert_dispatcher_matches_individual_hooks(payload_text, MULTI_EDIT_TOOL_NAME)
301
301
 
302
302
 
303
+ def test_dispatcher_docstring_points_at_roster_not_hardcoded_counts() -> None:
304
+ """The dispatcher docstring names the roster, not per-tool counts that drift."""
305
+ dispatcher_source = Path(_DISPATCHER_SCRIPT).read_text(encoding="utf-8")
306
+ assert "ALL_HOSTED_HOOK_ENTRIES" in dispatcher_source
307
+ assert "-> 20 hooks" not in dispatcher_source
308
+ assert "-> 21 hooks" not in dispatcher_source
309
+ assert "-> 9 hooks" not in dispatcher_source
310
+
311
+
303
312
  def test_multi_edit_runs_only_group_b_hooks() -> None:
304
313
  """Dispatcher invokes only Group-B hooks on MultiEdit, not Group-A hooks.
305
314
 
@@ -667,7 +676,7 @@ def test_proceed_after_run_all_validators_removal_allows() -> None:
667
676
  it was never a PreToolUse hook and never hosted by the PreToolUse dispatcher.
668
677
  A Python Write payload that run_all_validators would have flagged (mypy errors, for
669
678
  instance) still produces ALLOW from the PreToolUse dispatcher because the PreToolUse
670
- dispatcher covers only its 22 hosted blocking hooks — none of which includes the
679
+ dispatcher covers only its 23 hosted blocking hooks — none of which includes the
671
680
  validators runner.
672
681
  """
673
682
  python_content_with_type_error = (
@@ -0,0 +1,124 @@
1
+ """Tests for shell_substitution_blocker hook."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ SCRIPT_PATH = Path(__file__).parent / "shell_substitution_blocker.py"
10
+
11
+
12
+ def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
13
+ return subprocess.run(
14
+ [sys.executable, str(SCRIPT_PATH)],
15
+ input=json.dumps(payload),
16
+ text=True,
17
+ capture_output=True,
18
+ check=False,
19
+ )
20
+
21
+
22
+ def test_denies_dollar_paren_substitution() -> None:
23
+ payload = {
24
+ "tool_name": "Bash",
25
+ "tool_input": {"command": 'echo "head: $(git rev-parse HEAD)"'},
26
+ }
27
+ response = json.loads(_run_hook(payload).stdout)
28
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
29
+ assert (
30
+ "shell-substitution"
31
+ in response["hookSpecificOutput"]["permissionDecisionReason"]
32
+ )
33
+
34
+
35
+ def test_denies_unescaped_backtick_substitution() -> None:
36
+ payload = {
37
+ "tool_name": "Bash",
38
+ "tool_input": {"command": "echo `date`"},
39
+ }
40
+ response = json.loads(_run_hook(payload).stdout)
41
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
42
+
43
+
44
+ def test_allows_plain_command_without_substitution() -> None:
45
+ payload = {
46
+ "tool_name": "Bash",
47
+ "tool_input": {"command": "git rev-parse HEAD"},
48
+ }
49
+ assert _run_hook(payload).stdout == ""
50
+
51
+
52
+ def test_allows_escaped_backtick_in_prose() -> None:
53
+ payload = {
54
+ "tool_name": "Bash",
55
+ "tool_input": {"command": r'echo "use \`foo\` like this"'},
56
+ }
57
+ assert _run_hook(payload).stdout == ""
58
+
59
+
60
+ def test_ignores_non_bash_tool() -> None:
61
+ payload = {
62
+ "tool_name": "Edit",
63
+ "tool_input": {"command": 'echo "$(date)"'},
64
+ }
65
+ assert _run_hook(payload).stdout == ""
66
+
67
+
68
+ def test_denies_double_backslash_backtick_bypass() -> None:
69
+ payload = {
70
+ "tool_name": "Bash",
71
+ "tool_input": {"command": r"echo \\`date`"},
72
+ }
73
+ response = json.loads(_run_hook(payload).stdout)
74
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
75
+
76
+
77
+ def test_allows_dollar_paren_inside_single_quotes() -> None:
78
+ payload = {
79
+ "tool_name": "Bash",
80
+ "tool_input": {"command": "echo '$(not-executed)'"},
81
+ }
82
+ assert _run_hook(payload).stdout == ""
83
+
84
+
85
+ def test_allows_backtick_inside_single_quotes() -> None:
86
+ payload = {
87
+ "tool_name": "Bash",
88
+ "tool_input": {"command": "echo '`not-executed`'"},
89
+ }
90
+ assert _run_hook(payload).stdout == ""
91
+
92
+
93
+ def test_denies_input_process_substitution() -> None:
94
+ payload = {
95
+ "tool_name": "Bash",
96
+ "tool_input": {"command": "diff <(cat a) <(cat b)"},
97
+ }
98
+ response = json.loads(_run_hook(payload).stdout)
99
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
100
+
101
+
102
+ def test_denies_output_process_substitution() -> None:
103
+ payload = {
104
+ "tool_name": "Bash",
105
+ "tool_input": {"command": "tee >(gzip > out.gz)"},
106
+ }
107
+ response = json.loads(_run_hook(payload).stdout)
108
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
109
+
110
+
111
+ def test_allows_process_substitution_inside_single_quotes() -> None:
112
+ payload = {
113
+ "tool_name": "Bash",
114
+ "tool_input": {"command": "echo '<(not-executed)'"},
115
+ }
116
+ assert _run_hook(payload).stdout == ""
117
+
118
+
119
+ def test_allows_arithmetic_expansion() -> None:
120
+ payload = {
121
+ "tool_name": "Bash",
122
+ "tool_input": {"command": "echo $((2+2))"},
123
+ }
124
+ assert _run_hook(payload).stdout == ""
@@ -173,6 +173,29 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
173
173
  assert "probably" in parsed["reason"].lower() or "hedging" in parsed["reason"].lower()
174
174
 
175
175
 
176
+ def test_dispatcher_blocks_overlong_reply_matching_standalone() -> None:
177
+ """An overlong last_assistant_message blocks through the dispatcher."""
178
+ overlong_message = "\n".join(
179
+ " ".join(f"finding{each_index}" for each_index in range(10))
180
+ for _ in range(30)
181
+ )
182
+ payload_text = json.dumps(
183
+ {"stop_hook_active": False, "last_assistant_message": overlong_message}
184
+ )
185
+ completed = subprocess.run(
186
+ [sys.executable, _DISPATCHER_SCRIPT],
187
+ check=False,
188
+ input=payload_text,
189
+ capture_output=True,
190
+ text=True,
191
+ encoding="utf-8",
192
+ )
193
+ assert completed.returncode == 0
194
+ parsed = json.loads(completed.stdout)
195
+ assert parsed["decision"] == "block"
196
+ assert "ELI11 REPLY SHAPE" in parsed["reason"]
197
+
198
+
176
199
  def test_dispatcher_imports_standalone_with_only_blocking_on_the_path() -> None:
177
200
  """The dispatcher's bootstrap resolves hooks_constants without hooks/ on PYTHONPATH."""
178
201
  subprocess_environment = {**os.environ, "PYTHONPATH": str(_BLOCKING_DIR)}
@@ -0,0 +1,102 @@
1
+ """Tests for unscoped_search_blocker hook."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+
11
+ SCRIPT_PATH = Path(__file__).parent / "unscoped_search_blocker.py"
12
+
13
+ ALL_DENIED_COMMANDS = [
14
+ "find / -iname code_rules_gate.py",
15
+ "find /c -name '*.py'",
16
+ "find ~ -name README.md",
17
+ "find $HOME -type f",
18
+ "ls -R /",
19
+ "ls -laR /",
20
+ "ls -Rl /",
21
+ "find //server/share -name x",
22
+ "find \\\\server\\share -name x",
23
+ "bash -c 'find / -name x'",
24
+ ]
25
+
26
+ ALL_ALLOWED_COMMANDS = [
27
+ "find . -iname '*.py'",
28
+ "find packages/claude-dev-env -name code_rules_gate.py",
29
+ "find /c/Users/example/repo -iname SKILL.md",
30
+ "ls -r /",
31
+ "ls -la /tmp/x",
32
+ "find //server/share/project/src -name x",
33
+ "es.exe path:C:\\dev\\repo ext:py gate",
34
+ "git status --short",
35
+ ]
36
+
37
+
38
+ def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
39
+ return subprocess.run(
40
+ [sys.executable, str(SCRIPT_PATH)],
41
+ input=json.dumps(payload),
42
+ text=True,
43
+ capture_output=True,
44
+ check=False,
45
+ )
46
+
47
+
48
+ def _bash_payload(command: str) -> dict:
49
+ return {"tool_name": "Bash", "tool_input": {"command": command}}
50
+
51
+
52
+ def _powershell_payload(command: str) -> dict:
53
+ return {"tool_name": "PowerShell", "tool_input": {"command": command}}
54
+
55
+
56
+ @pytest.mark.parametrize("each_command", ALL_DENIED_COMMANDS)
57
+ def test_denies_unscoped_root_walk(each_command: str) -> None:
58
+ response = json.loads(_run_hook(_bash_payload(each_command)).stdout)
59
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
60
+
61
+
62
+ @pytest.mark.parametrize("each_command", ALL_ALLOWED_COMMANDS)
63
+ def test_allows_scoped_search(each_command: str) -> None:
64
+ assert _run_hook(_bash_payload(each_command)).stdout == ""
65
+
66
+
67
+ def test_denies_recursive_listing_on_windows_drive_root() -> None:
68
+ response = json.loads(
69
+ _run_hook(_powershell_payload("Get-ChildItem -Path C:\\ -Recurse")).stdout
70
+ )
71
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
72
+
73
+
74
+ def test_allows_recursive_listing_under_project_directory() -> None:
75
+ assert _run_hook(_powershell_payload("Get-ChildItem -Path .\\src -Recurse")).stdout == ""
76
+
77
+
78
+ def test_allows_recursive_listing_without_recurse_flag() -> None:
79
+ assert _run_hook(_powershell_payload("Get-ChildItem -Path C:\\")).stdout == ""
80
+
81
+
82
+ def test_denies_find_root_in_a_chained_segment() -> None:
83
+ response = json.loads(
84
+ _run_hook(_bash_payload("cd /tmp && find / -name x.py")).stdout
85
+ )
86
+ assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
87
+
88
+
89
+ def test_ignores_unsupported_tool_name() -> None:
90
+ payload = {"tool_name": "Edit", "tool_input": {"command": "find / -name x"}}
91
+ assert _run_hook(payload).stdout == ""
92
+
93
+
94
+ def test_ignores_payload_without_a_command() -> None:
95
+ payload = {"tool_name": "Bash", "tool_input": {}}
96
+ assert _run_hook(payload).stdout == ""
97
+
98
+
99
+ def test_deny_message_names_the_scoped_alternative() -> None:
100
+ response = json.loads(_run_hook(_bash_payload("find / -name x.py")).stdout)
101
+ deny_reason = response["hookSpecificOutput"]["permissionDecisionReason"]
102
+ assert "scope" in deny_reason.lower()