claude-dev-env 2.12.0 → 2.14.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 (399) hide show
  1. package/AGENTS.md +61 -0
  2. package/CLAUDE.md +1 -57
  3. package/_shared/AGENTS.md +15 -0
  4. package/_shared/CLAUDE.md +1 -15
  5. package/_shared/advisor/AGENTS.md +22 -0
  6. package/_shared/advisor/CLAUDE.md +1 -22
  7. package/_shared/pr-loop/AGENTS.md +27 -0
  8. package/_shared/pr-loop/CLAUDE.md +1 -27
  9. package/_shared/pr-loop/scripts/AGENTS.md +35 -0
  10. package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
  16. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  17. package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
  18. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
  19. package/_shared/process-tree/AGENTS.md +41 -0
  20. package/_shared/process-tree/CLAUDE.md +1 -41
  21. package/agents/AGENTS.md +31 -0
  22. package/agents/CLAUDE.md +1 -32
  23. package/agents/clean-coder.md +6 -6
  24. package/agents/pr-description-writer.md +11 -81
  25. package/agents/test_agent_frontmatter.py +4 -4
  26. package/audit-rubrics/AGENTS.md +43 -0
  27. package/audit-rubrics/CLAUDE.md +1 -43
  28. package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
  29. package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
  30. package/audit-rubrics/prompts/AGENTS.md +37 -0
  31. package/audit-rubrics/prompts/CLAUDE.md +1 -37
  32. package/bin/AGENTS.md +112 -0
  33. package/bin/CLAUDE.md +1 -112
  34. package/bin/ever-shipped-skills.mjs +1 -0
  35. package/bin/install-constants.mjs +6 -7
  36. package/bin/install.mjs +71 -13
  37. package/bin/install.prune.test.mjs +26 -0
  38. package/bin/install.settings-defaults.test.mjs +60 -33
  39. package/bin/install.test.mjs +36 -4
  40. package/commands/AGENTS.md +23 -0
  41. package/commands/CLAUDE.md +1 -23
  42. package/commands/hook-log-extract.md +3 -3
  43. package/commands/sr-loop.md +9 -5
  44. package/docs/AGENTS.md +32 -0
  45. package/docs/CLAUDE.md +1 -33
  46. package/docs/CODE_RULES.md +1 -1
  47. package/docs/imagegen.md +22 -0
  48. package/docs/references/AGENTS.md +17 -0
  49. package/docs/references/CLAUDE.md +1 -18
  50. package/hooks/AGENTS.md +31 -0
  51. package/hooks/CLAUDE.md +1 -31
  52. package/hooks/_gh_pr_author_swap_utils.py +14 -0
  53. package/hooks/advisory/AGENTS.md +16 -0
  54. package/hooks/advisory/CLAUDE.md +1 -16
  55. package/hooks/blocking/AGENTS.md +123 -0
  56. package/hooks/blocking/CLAUDE.md +1 -148
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
  59. package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
  60. package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
  61. package/hooks/blocking/code_rules_probe_chains.py +6 -2
  62. package/hooks/blocking/config/AGENTS.md +10 -0
  63. package/hooks/blocking/config/CLAUDE.md +1 -22
  64. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  65. package/hooks/blocking/gh_pr_author_restore.py +7 -2
  66. package/hooks/blocking/intent_only_ending_blocker.py +11 -16
  67. package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
  69. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
  70. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
  71. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
  72. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
  73. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  74. package/hooks/blocking/stop_dispatcher.py +4 -4
  75. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
  76. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
  77. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  78. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
  80. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  81. package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
  82. package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
  83. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  84. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  85. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  86. package/hooks/diagnostic/AGENTS.md +43 -0
  87. package/hooks/diagnostic/CLAUDE.md +1 -43
  88. package/hooks/diagnostic/hook_log_extractor.py +4 -38
  89. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  90. package/hooks/diagnostic/migrations/AGENTS.md +16 -0
  91. package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
  92. package/hooks/diagnostic/queries/AGENTS.md +19 -0
  93. package/hooks/diagnostic/queries/CLAUDE.md +1 -19
  94. package/hooks/diagnostic/test_hook_log_extractor.py +10 -23
  95. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  96. package/hooks/git-hooks/AGENTS.md +31 -0
  97. package/hooks/git-hooks/CLAUDE.md +1 -31
  98. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
  99. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -12
  101. package/hooks/git-hooks/pre_push.py +49 -118
  102. package/hooks/git-hooks/test_pre_push.py +111 -137
  103. package/hooks/hooks.json +0 -35
  104. package/hooks/hooks_constants/AGENTS.md +104 -0
  105. package/hooks/hooks_constants/CLAUDE.md +1 -113
  106. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  107. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  108. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  109. package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
  110. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  111. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  112. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  113. package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
  114. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  115. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  117. package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
  118. package/hooks/lifecycle/AGENTS.md +18 -0
  119. package/hooks/lifecycle/CLAUDE.md +1 -20
  120. package/hooks/observability/AGENTS.md +20 -0
  121. package/hooks/observability/CLAUDE.md +1 -20
  122. package/hooks/session/AGENTS.md +33 -0
  123. package/hooks/session/CLAUDE.md +1 -33
  124. package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
  125. package/hooks/session/task_list_loop_starter.py +3 -3
  126. package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
  127. package/hooks/session/test_task_list_loop_starter.py +10 -2
  128. package/hooks/session/test_working_style_prompt.py +9 -4
  129. package/hooks/validation/AGENTS.md +20 -0
  130. package/hooks/validation/CLAUDE.md +1 -20
  131. package/hooks/validators/AGENTS.md +51 -0
  132. package/hooks/validators/CLAUDE.md +1 -51
  133. package/hooks/validators/health_check.py +52 -19
  134. package/hooks/validators/python_style_checks.py +1 -1
  135. package/hooks/validators/test_health_check.py +112 -27
  136. package/hooks/workflow/AGENTS.md +16 -0
  137. package/hooks/workflow/CLAUDE.md +1 -16
  138. package/installable-surfaces.manifest.json +1 -0
  139. package/output-styles/AGENTS.md +15 -0
  140. package/output-styles/CLAUDE.md +1 -15
  141. package/package.json +2 -1
  142. package/rules/AGENTS.md +61 -0
  143. package/rules/CLAUDE.md +1 -63
  144. package/rules/code-standards.md +5 -9
  145. package/rules/doc-prose-cuts.md +1 -1
  146. package/rules/gh-cli-conventions.md +0 -1
  147. package/rules/git-workflow.md +1 -3
  148. package/scripts/AGENTS.md +62 -0
  149. package/scripts/CLAUDE.md +1 -63
  150. package/scripts/_code_review_test_support.py +0 -95
  151. package/scripts/codex_compat_materializer.py +4 -0
  152. package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
  153. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
  154. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  155. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  156. package/scripts/invoke_code_review.py +8 -451
  157. package/scripts/sync_to_cursor/AGENTS.md +23 -0
  158. package/scripts/sync_to_cursor/CLAUDE.md +1 -23
  159. package/scripts/test_invoke_code_review.py +5 -61
  160. package/scripts/test_invoke_code_review_cli.py +1 -45
  161. package/scripts/test_invoke_code_review_contract.py +1 -118
  162. package/scripts/test_validate_instruction_pairs.py +90 -0
  163. package/scripts/test_verify_installable_package.py +5 -1
  164. package/scripts/tests/AGENTS.md +33 -0
  165. package/scripts/tests/CLAUDE.md +1 -33
  166. package/scripts/tests/test_codex_compat_materializer.py +36 -1
  167. package/scripts/validate_instruction_pairs.py +183 -0
  168. package/settings.json +1 -10
  169. package/skills/AGENTS.md +72 -0
  170. package/skills/CLAUDE.md +1 -64
  171. package/skills/_shared/AGENTS.md +44 -0
  172. package/skills/_shared/CLAUDE.md +1 -44
  173. package/skills/_shared/advisor/AGENTS.md +9 -0
  174. package/skills/_shared/advisor/CLAUDE.md +1 -9
  175. package/skills/_shared/pr-loop/AGENTS.md +50 -0
  176. package/skills/_shared/pr-loop/CLAUDE.md +1 -50
  177. package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
  178. package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
  179. package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
  180. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  181. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
  182. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
  183. package/skills/anthropic-plan/AGENTS.md +34 -0
  184. package/skills/anthropic-plan/CLAUDE.md +1 -34
  185. package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
  186. package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
  187. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
  188. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
  189. package/skills/anthropic-plan/templates/AGENTS.md +13 -0
  190. package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
  191. package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
  192. package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
  193. package/skills/auditing-claude-config/AGENTS.md +21 -0
  194. package/skills/auditing-claude-config/CLAUDE.md +1 -21
  195. package/skills/autoconverge/AGENTS.md +36 -0
  196. package/skills/autoconverge/CLAUDE.md +1 -36
  197. package/skills/autoconverge/reference/AGENTS.md +16 -0
  198. package/skills/autoconverge/reference/CLAUDE.md +1 -16
  199. package/skills/autoconverge/reference/convergence.md +7 -7
  200. package/skills/autoconverge/reference/gotchas.md +3 -10
  201. package/skills/autoconverge/workflow/AGENTS.md +26 -0
  202. package/skills/autoconverge/workflow/CLAUDE.md +1 -26
  203. package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
  204. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
  205. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  206. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  207. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  208. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  209. package/skills/autoconverge/workflow/converge.mjs +201 -238
  210. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  211. package/skills/bugteam/AGENTS.md +30 -0
  212. package/skills/bugteam/CLAUDE.md +1 -30
  213. package/skills/bugteam/CONSTRAINTS.md +1 -1
  214. package/skills/bugteam/SKILL.md +9 -0
  215. package/skills/bugteam/reference/AGENTS.md +20 -0
  216. package/skills/bugteam/reference/CLAUDE.md +1 -20
  217. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  218. package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
  219. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
  220. package/skills/bugteam/scripts/AGENTS.md +30 -0
  221. package/skills/bugteam/scripts/CLAUDE.md +1 -30
  222. package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
  223. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
  224. package/skills/bugteam/test_skill_additions.py +9 -0
  225. package/skills/closeout/reference/handoff-prompt-template.md +1 -1
  226. package/skills/codex-review/AGENTS.md +46 -0
  227. package/skills/codex-review/CLAUDE.md +1 -46
  228. package/skills/codex-review/reference/AGENTS.md +15 -0
  229. package/skills/codex-review/reference/CLAUDE.md +1 -15
  230. package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
  231. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
  232. package/skills/codex-review/test_skill_scaffold.py +3 -3
  233. package/skills/comments/SKILL.md +65 -0
  234. package/skills/copilot-review/AGENTS.md +18 -0
  235. package/skills/copilot-review/CLAUDE.md +1 -18
  236. package/skills/descriptions/SKILL.md +51 -0
  237. package/skills/emergencies/SKILL.md +42 -0
  238. package/skills/everything-search/AGENTS.md +17 -0
  239. package/skills/everything-search/CLAUDE.md +1 -17
  240. package/skills/fresh-branch/AGENTS.md +14 -0
  241. package/skills/fresh-branch/CLAUDE.md +1 -14
  242. package/skills/grok-spawn/AGENTS.md +28 -0
  243. package/skills/grok-spawn/CLAUDE.md +1 -28
  244. package/skills/orchestrator/SKILL.md +2 -4
  245. package/skills/plan-to-pr/SKILL.md +6 -7
  246. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  247. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  248. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  249. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  250. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  251. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  252. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  253. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  254. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  255. package/skills/pr-converge/AGENTS.md +32 -0
  256. package/skills/pr-converge/CLAUDE.md +1 -32
  257. package/skills/pr-converge/SKILL.md +13 -16
  258. package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
  259. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
  260. package/skills/pr-converge/reference/AGENTS.md +28 -0
  261. package/skills/pr-converge/reference/CLAUDE.md +1 -28
  262. package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
  263. package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
  264. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  265. package/skills/pr-converge/reference/state-schema.md +1 -27
  266. package/skills/pr-converge/scripts/AGENTS.md +46 -0
  267. package/skills/pr-converge/scripts/CLAUDE.md +1 -46
  268. package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
  269. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
  270. package/skills/pr-converge/workflows/AGENTS.md +16 -0
  271. package/skills/pr-converge/workflows/CLAUDE.md +1 -16
  272. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  273. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  274. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  275. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  276. package/skills/prototype/SKILL.md +4 -4
  277. package/skills/prototype/reference/honest-limitations.md +5 -5
  278. package/skills/prototype/reference/promotion-tasks.md +2 -2
  279. package/skills/prototype/workflows/promotion.md +4 -6
  280. package/skills/rebase/AGENTS.md +32 -0
  281. package/skills/rebase/CLAUDE.md +1 -32
  282. package/skills/recall/AGENTS.md +30 -0
  283. package/skills/recall/CLAUDE.md +1 -30
  284. package/skills/remember/AGENTS.md +31 -0
  285. package/skills/remember/CLAUDE.md +1 -31
  286. package/skills/reviews/SKILL.md +85 -0
  287. package/skills/session-log/AGENTS.md +32 -0
  288. package/skills/session-log/CLAUDE.md +1 -32
  289. package/skills/session-tidy/AGENTS.md +36 -0
  290. package/skills/session-tidy/CLAUDE.md +1 -36
  291. package/skills/skill-builder/AGENTS.md +49 -0
  292. package/skills/skill-builder/CLAUDE.md +1 -49
  293. package/skills/skill-builder/references/AGENTS.md +25 -0
  294. package/skills/skill-builder/references/CLAUDE.md +1 -25
  295. package/skills/skill-builder/references/deterministic-elements.md +1 -1
  296. package/skills/skill-builder/templates/AGENTS.md +14 -0
  297. package/skills/skill-builder/templates/CLAUDE.md +1 -14
  298. package/skills/skill-builder/workflows/AGENTS.md +19 -0
  299. package/skills/skill-builder/workflows/CLAUDE.md +1 -19
  300. package/skills/small-cl/SKILL.md +52 -0
  301. package/skills/task-build/AGENTS.md +29 -0
  302. package/skills/task-build/CLAUDE.md +1 -29
  303. package/skills/update/AGENTS.md +38 -0
  304. package/skills/update/CLAUDE.md +1 -38
  305. package/system-prompts/AGENTS.md +17 -0
  306. package/system-prompts/CLAUDE.md +1 -17
  307. package/system-prompts/software-engineer.xml +3 -3
  308. package/agents/code-verifier.md +0 -74
  309. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  310. package/docs/references/code-review-enforcement.md +0 -132
  311. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  312. package/hooks/blocking/code_review_gate_deny.py +0 -74
  313. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  314. package/hooks/blocking/code_review_push_gate.py +0 -145
  315. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  316. package/hooks/blocking/code_review_stamp_store.py +0 -233
  317. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  318. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  319. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  320. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  321. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  322. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  323. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  324. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  325. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  326. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  327. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  328. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  329. package/hooks/blocking/conftest.py +0 -123
  330. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  331. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  332. package/hooks/blocking/pr_description_body_audit.py +0 -148
  333. package/hooks/blocking/pr_description_command_parser.py +0 -237
  334. package/hooks/blocking/pr_description_enforcer.py +0 -227
  335. package/hooks/blocking/pr_description_pr_number.py +0 -155
  336. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  337. package/hooks/blocking/pr_description_readability.py +0 -364
  338. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  339. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  340. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  341. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  342. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  343. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  344. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  345. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  346. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  347. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  348. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  349. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  350. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  351. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  352. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  353. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  354. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  355. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  356. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  357. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  358. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  359. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  360. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  361. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  362. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  363. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  364. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  365. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  366. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  367. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  368. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  369. package/hooks/blocking/verification_verdict_store.py +0 -1039
  370. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  371. package/hooks/blocking/verified_commit_gate.py +0 -113
  372. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  373. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  374. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  375. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  376. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  377. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  378. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  379. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  380. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  381. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  382. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  383. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  384. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  385. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  386. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  387. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  388. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  389. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  390. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  391. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  392. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  393. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  394. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  395. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  396. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  397. package/rules/proof-of-work-pr-comments.md +0 -22
  398. package/rules/verified-commit-gate-skip.md +0 -4
  399. package/scripts/Show-Asset.ps1 +0 -106
@@ -1,687 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse guard: deny shell access to the verification verdict directory.
3
-
4
- The verified-commit gate trusts a single invariant — only
5
- ``verifier_verdict_minter.py`` (a SubagentStop hook running in-process)
6
- writes verdict files. settings.json denies the Write/Edit/MultiEdit file
7
- tools against ``~/.claude/verification/**``, but a Bash or PowerShell
8
- command reaches the same directory through ``python -c``, a redirect, or an
9
- ``Out-File``/``Set-Content`` cmdlet. A forged verdict only needs the file to
10
- exist with ``all_pass`` true and the live manifest hash — both values the
11
- session can derive — so an unguarded shell write defeats the whole gate.
12
-
13
- This guard fires on Bash and PowerShell tool calls and denies any command
14
- whose text references the verdict directory — whether by an absolute
15
- ``.claude/verification/`` path, by changing into the Claude home directory
16
- and then writing a relative ``verification/`` path, by naming a verdict
17
- file's ``verification/<root-key>.json`` shape directly, or by pairing a
18
- path-obfuscation primitive (``chr(``, ``bytes.fromhex(``, base64 decode,
19
- ``codecs.decode(``, a ``bytes([...])``/``bytearray([...])`` int-list
20
- constructor) with a file write so a path assembled from codes cannot
21
- slip past the literal matchers. No legitimate workflow reaches that
22
- directory through a shell: the minter runs in-process and the gate reads
23
- verdicts in-process, so blocking every shell reference is the fail-closed
24
- stance that keeps the verdict store unforgeable.
25
- """
26
-
27
- from __future__ import annotations
28
-
29
- import base64
30
- import binascii
31
- import json
32
- import re
33
- import sys
34
- from pathlib import Path
35
-
36
- blocking_directory = str(Path(__file__).resolve().parent)
37
- if blocking_directory not in sys.path:
38
- sys.path.insert(0, blocking_directory)
39
-
40
- hooks_directory = str(Path(__file__).resolve().parent.parent)
41
- if hooks_directory not in sys.path:
42
- sys.path.append(hooks_directory)
43
-
44
- from verified_commit_config_bootstrap import ( # noqa: E402
45
- register_verified_commit_constants,
46
- )
47
-
48
- register_verified_commit_constants()
49
-
50
- from config.verified_commit_constants import ( # noqa: E402
51
- ALL_GATED_TOOL_NAMES,
52
- ALL_VERDICT_PATH_SEGMENT_BODIES,
53
- ALL_VERDICT_PATH_SEGMENT_NAMES,
54
- BASE64_TOKEN_PATTERN,
55
- CHARACTER_CODE_SEQUENCE_PATTERN,
56
- CHR_CALL_CHAIN_PATTERN,
57
- CHR_CALL_CODE_PATTERN,
58
- CLAUDE_HOME_DIRECTORY_NAME,
59
- CLAUDE_HOME_TARGET_BOUNDARY_PATTERN,
60
- COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN,
61
- DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN,
62
- DIRECTORY_CHANGE_OPTION_TERMINATOR,
63
- DIRECTORY_CHANGE_PATH_OPTIONS,
64
- DIRECTORY_CHANGE_PATTERN_PREFIX,
65
- DIRECTORY_CHANGE_PATTERN_SUFFIX,
66
- DIRECTORY_CHANGE_TARGET_PATTERN,
67
- DIRECTORY_CHANGE_VERBS,
68
- FILE_WRITE_PRIMITIVE_PATTERN,
69
- HEX_DIGITS_PER_BYTE,
70
- HEX_TOKEN_PATTERN,
71
- NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN,
72
- PATH_OBFUSCATION_PRIMITIVE_PATTERN,
73
- RELATIVE_VERDICT_DIRECTORY_PATTERN,
74
- VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN,
75
- VERDICT_DIRECTORY_GUARD_MESSAGE,
76
- VERDICT_DIRECTORY_NAME,
77
- VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN,
78
- VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN,
79
- VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN,
80
- VERDICT_FILE_RELATIVE_REFERENCE_PATTERN,
81
- VERDICT_PATH_GLUE_PATTERN,
82
- VERDICT_PATH_JOINED_VARIABLE_PATTERN,
83
- VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN,
84
- WRITE_CALL_REGION_PATTERN,
85
- )
86
-
87
- from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
88
- from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
89
- read_hook_input_dictionary_from_stdin,
90
- )
91
-
92
-
93
- def _directory_change_verbs_pattern() -> str:
94
- """Build the alternation of directory-change verbs for a change matcher.
95
-
96
- Returns:
97
- The regex alternation of escaped directory-change verbs in sorted
98
- order, ready to sit between the change prefix and suffix patterns.
99
- """
100
- return "|".join(re.escape(each_verb) for each_verb in sorted(DIRECTORY_CHANGE_VERBS))
101
-
102
-
103
- def _directory_change_option_prefix_pattern() -> str:
104
- """Build the optional path-option prefix that may precede a change target.
105
-
106
- A PowerShell directory change names its destination after a path flag
107
- (``Set-Location -Path ~/.claude/verification``,
108
- ``Set-Location -LiteralPath ...``), and a POSIX ``cd`` may guard the
109
- destination with the ``--`` end-of-options terminator
110
- (``cd -- ~/.claude/verification``). Consuming any run of those tokens
111
- before the target keeps the flag from being read as the destination, so
112
- the change matchers see the real path the same way the sibling gate's
113
- ``_directory_change_target`` token-walk does.
114
-
115
- Returns:
116
- The regex matching zero or more leading path-option or terminator
117
- tokens, ready to sit between the change suffix and target patterns.
118
- """
119
- option_alternation = "|".join(
120
- re.escape(each_option)
121
- for each_option in sorted(DIRECTORY_CHANGE_PATH_OPTIONS | {DIRECTORY_CHANGE_OPTION_TERMINATOR})
122
- )
123
- return DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN % option_alternation
124
-
125
-
126
- def _references_absolute_verdict_path(command_text: str) -> bool:
127
- """Decide whether a command names an absolute ``.claude/verification/`` path.
128
-
129
- Matches the Claude-home/verdict-directory name pair joined by any run of
130
- path separators, quotes, commas, or whitespace, anchored by a trailing
131
- path separator. That separator class catches every home prefix form
132
- (``~``, ``$HOME``, an expanded absolute path), either path separator, and
133
- the ``joinpath('.claude', 'verification', ...)`` / ``os.path.join``
134
- spellings; the trailing path boundary keeps sibling directories
135
- (``verification-docs``) and bare prose mentions of the path from matching.
136
-
137
- Args:
138
- command_text: The raw command string from the tool payload.
139
-
140
- Returns:
141
- True when the command names an absolute verdict-directory path.
142
- """
143
- verdict_directory_pattern = re.compile(
144
- re.escape(CLAUDE_HOME_DIRECTORY_NAME)
145
- + VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN
146
- + re.escape(VERDICT_DIRECTORY_NAME)
147
- + VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN,
148
- re.IGNORECASE,
149
- )
150
- return verdict_directory_pattern.search(command_text) is not None
151
-
152
-
153
- def _changes_into_claude_home_then_writes_relative(command_text: str) -> bool:
154
- """Decide whether a command enters the Claude home then writes ``verification/``.
155
-
156
- A ``cd ~/.claude`` (or ``pushd``/``Set-Location``/``sl`` into any path
157
- ending in ``.claude``) followed by a relative ``verification/`` write
158
- lands in the verdict directory without ever naming the two segments
159
- adjacently, so the absolute-path matcher alone misses it.
160
-
161
- Args:
162
- command_text: The raw command string from the tool payload.
163
-
164
- Returns:
165
- True when a directory change into ``.claude`` precedes a relative
166
- verdict-directory reference.
167
- """
168
- directory_change_into_claude_pattern = re.compile(
169
- DIRECTORY_CHANGE_PATTERN_PREFIX
170
- + _directory_change_verbs_pattern()
171
- + DIRECTORY_CHANGE_PATTERN_SUFFIX
172
- + _directory_change_option_prefix_pattern()
173
- + DIRECTORY_CHANGE_TARGET_PATTERN
174
- + re.escape(CLAUDE_HOME_DIRECTORY_NAME)
175
- + CLAUDE_HOME_TARGET_BOUNDARY_PATTERN,
176
- re.IGNORECASE,
177
- )
178
- change_match = directory_change_into_claude_pattern.search(command_text)
179
- if change_match is None:
180
- return False
181
- relative_verdict_pattern = re.compile(RELATIVE_VERDICT_DIRECTORY_PATTERN, re.IGNORECASE)
182
- return relative_verdict_pattern.search(command_text, change_match.end()) is not None
183
-
184
-
185
- def _changes_into_verdict_directory_then_writes(command_text: str) -> bool:
186
- """Decide whether a command enters the verdict directory then runs a command.
187
-
188
- A ``cd ~/.claude/verification`` (or ``pushd``/``Set-Location``/``sl`` into
189
- any path ending in ``.claude/verification``, with or without a trailing
190
- separator) followed by any further command lands a forged verdict in the
191
- verdict directory without naming the ``verification/`` prefix on the file,
192
- so the other matchers miss it. The trust contract denies every shell
193
- command that reaches the verdict directory, so any command after the
194
- change — a redirect, ``cp``, ``mv``, ``tee``, ``install``, or a
195
- ``python -c`` write — is a forge vector. Matching any command separator
196
- followed by content keeps the guard write-agnostic rather than chasing an
197
- open-ended verb list.
198
-
199
- Args:
200
- command_text: The raw command string from the tool payload.
201
-
202
- Returns:
203
- True when a directory change into the verdict directory precedes any
204
- further command.
205
- """
206
- directory_change_into_verdict_pattern = re.compile(
207
- DIRECTORY_CHANGE_PATTERN_PREFIX
208
- + _directory_change_verbs_pattern()
209
- + DIRECTORY_CHANGE_PATTERN_SUFFIX
210
- + _directory_change_option_prefix_pattern()
211
- + DIRECTORY_CHANGE_TARGET_PATTERN
212
- + re.escape(CLAUDE_HOME_DIRECTORY_NAME)
213
- + r"[\\/]"
214
- + re.escape(VERDICT_DIRECTORY_NAME)
215
- + VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN,
216
- re.IGNORECASE,
217
- )
218
- change_match = directory_change_into_verdict_pattern.search(command_text)
219
- if change_match is None:
220
- return False
221
- command_after_change_pattern = re.compile(COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN)
222
- return command_after_change_pattern.search(command_text, change_match.end()) is not None
223
-
224
-
225
- def _references_verdict_file_shape(command_text: str) -> bool:
226
- """Decide whether a command names a verdict file's ``verification/<key>.json`` shape.
227
-
228
- A verdict file name is ``verification/<root-key-hex>.json``, where the
229
- root key is the leading hex digits of a repo-path digest. Naming that
230
- shape relative to any directory is a forge vector on its own, so it is
231
- flagged regardless of a preceding directory change.
232
-
233
- Args:
234
- command_text: The raw command string from the tool payload.
235
-
236
- Returns:
237
- True when the command names the verdict-file shape.
238
- """
239
- verdict_file_pattern = re.compile(VERDICT_FILE_RELATIVE_REFERENCE_PATTERN, re.IGNORECASE)
240
- return verdict_file_pattern.search(command_text) is not None
241
-
242
-
243
- def _decoded_texts_from_command(command_text: str) -> list[str]:
244
- """Decode every hex, base64, character-code, and chr-chain token a command embeds.
245
-
246
- A path-obfuscation forge hides ``.claude`` or ``verification`` inside hex
247
- digits, a base64 token, a comma-separated character-code sequence, or a
248
- run of ``+``-joined ``chr(<int>)`` calls. Decoding each token back to text
249
- lets the segment matcher see the hidden name. Tokens that fail to decode or
250
- carry an out-of-range code are skipped.
251
-
252
- Args:
253
- command_text: The raw command string from the tool payload.
254
-
255
- Returns:
256
- The lowercase decoded text for every token that decodes cleanly.
257
- """
258
- all_decoded_texts: list[str] = []
259
- for each_hex_token in re.findall(HEX_TOKEN_PATTERN, command_text, re.IGNORECASE):
260
- decoded_hex_text = _decode_hex_token(each_hex_token)
261
- if decoded_hex_text:
262
- all_decoded_texts.append(decoded_hex_text.lower())
263
- for each_base64_token in re.findall(BASE64_TOKEN_PATTERN, command_text):
264
- decoded_base64_text = _decode_base64_token(each_base64_token)
265
- if decoded_base64_text:
266
- all_decoded_texts.append(decoded_base64_text.lower())
267
- for each_code_sequence in re.findall(CHARACTER_CODE_SEQUENCE_PATTERN, command_text):
268
- decoded_code_text = _decode_character_code_sequence(each_code_sequence)
269
- if decoded_code_text:
270
- all_decoded_texts.append(decoded_code_text.lower())
271
- for each_chr_chain in re.findall(CHR_CALL_CHAIN_PATTERN, command_text, re.IGNORECASE):
272
- decoded_chain_text = _decode_chr_call_chain(each_chr_chain)
273
- if decoded_chain_text:
274
- all_decoded_texts.append(decoded_chain_text.lower())
275
- return all_decoded_texts
276
-
277
-
278
- def _decode_hex_token(hex_token: str) -> str:
279
- """Decode a hex-digit token to text, returning empty text on failure.
280
-
281
- Args:
282
- hex_token: A run of hex digits taken from the command.
283
-
284
- Returns:
285
- The decoded ASCII text, or empty text when the token is odd-length,
286
- not valid hex, or not decodable as ASCII.
287
- """
288
- if len(hex_token) % HEX_DIGITS_PER_BYTE == 1:
289
- return ""
290
- try:
291
- return bytes.fromhex(hex_token).decode("ascii")
292
- except (ValueError, binascii.Error):
293
- return ""
294
-
295
-
296
- def _decode_base64_token(base64_token: str) -> str:
297
- """Decode a base64 token to text, returning empty text on failure.
298
-
299
- Args:
300
- base64_token: A base64-shaped token taken from the command.
301
-
302
- Returns:
303
- The decoded ASCII text, or empty text when the token is not valid
304
- base64 or not decodable as ASCII.
305
- """
306
- try:
307
- return base64.b64decode(base64_token, validate=True).decode("ascii")
308
- except (ValueError, binascii.Error):
309
- return ""
310
-
311
-
312
- def _decode_character_code_sequence(code_sequence: str) -> str:
313
- """Decode a comma-separated character-code sequence to text.
314
-
315
- The ``CHARACTER_CODE_SEQUENCE_PATTERN`` source caps each code at three
316
- decimal digits, so every code is a valid ``chr`` argument and decoding
317
- always succeeds.
318
-
319
- Args:
320
- code_sequence: A comma-separated run of decimal character codes, each
321
- within the three-digit range the source pattern admits.
322
-
323
- Returns:
324
- The decoded text.
325
- """
326
- decoded_characters: list[str] = []
327
- for each_code in code_sequence.split(","):
328
- code_point = int(each_code.strip())
329
- decoded_characters.append(chr(code_point))
330
- return "".join(decoded_characters)
331
-
332
-
333
- def _decode_chr_call_chain(chr_chain: str) -> str:
334
- """Decode a run of ``+``-joined ``chr(<int>)`` calls to text.
335
-
336
- A forge assembles ``/.claude/verification/`` from a run of
337
- ``chr(<code>)+chr(<code>)+...`` calls so the path carries no comma-separated
338
- code list and no literal segment name. The ``CHR_CALL_CHAIN_PATTERN``
339
- source caps each code at three decimal digits, so every captured int is a
340
- valid ``chr`` argument and decoding always succeeds.
341
-
342
- Args:
343
- chr_chain: A run of two or more ``+``-joined ``chr(<int>)`` calls
344
- taken from the command.
345
-
346
- Returns:
347
- The decoded text.
348
- """
349
- decoded_characters: list[str] = []
350
- for each_code in re.findall(CHR_CALL_CODE_PATTERN, chr_chain, re.IGNORECASE):
351
- decoded_characters.append(chr(int(each_code)))
352
- return "".join(decoded_characters)
353
-
354
-
355
- def _decoded_token_references_verdict_segment(command_text: str) -> bool:
356
- """Decide whether a decodable token in a command hides a verdict-path segment.
357
-
358
- A forge hides ``.claude`` or ``verification`` inside a hex, base64,
359
- character-code, or ``+``-joined ``chr(<int>)`` chain token
360
- (``bytes.fromhex('2e636c61756465')`` and a ``chr(<code>)+chr(<code>)+...``
361
- run both decode to a ``.claude`` segment). Such a token carries no
362
- incidental meaning — text that decodes to a verdict-path segment body
363
- (``claude``) is forge evidence wherever it sits in the command, so this
364
- check is not scoped to the write call.
365
-
366
- Args:
367
- command_text: The raw command string from the tool payload.
368
-
369
- Returns:
370
- True when a decodable token decodes to a verdict-path segment body.
371
- """
372
- all_decoded_texts = _decoded_texts_from_command(command_text)
373
- for each_decoded_text in all_decoded_texts:
374
- for each_segment_body in ALL_VERDICT_PATH_SEGMENT_BODIES:
375
- if each_segment_body in each_decoded_text:
376
- return True
377
- return False
378
-
379
-
380
- def _write_call_region_names_verdict_segment(command_text: str) -> bool:
381
- """Decide whether a write call's argument region names a verdict-path segment.
382
-
383
- The region runs from a non-redirect write primitive (``open(``,
384
- ``write_text``, ``Out-File``, ``Set-Content``, ``Add-Content``, ``tee``)
385
- to the next statement separator (``;``, ``|``, ``&``, newline) or the end
386
- of the command. A plain-text ``.claude`` or ``verification`` inside that
387
- region is part of the path being written, so it is a forge signal; the
388
- same word in a later statement (``open(chr(...)+chr(...));
389
- print('verification done')``) sits past the separator and does not count.
390
-
391
- Args:
392
- command_text: The raw command string from the tool payload.
393
-
394
- Returns:
395
- True when a verdict-path segment name appears inside a write call's
396
- argument region.
397
- """
398
- write_call_region_pattern = re.compile(WRITE_CALL_REGION_PATTERN, re.IGNORECASE)
399
- for each_region_match in write_call_region_pattern.finditer(command_text):
400
- lowercased_region = each_region_match.group(0).lower()
401
- for each_segment_name in ALL_VERDICT_PATH_SEGMENT_NAMES:
402
- if each_segment_name in lowercased_region:
403
- return True
404
- return False
405
-
406
-
407
- def _uses_obfuscated_path_write(command_text: str) -> bool:
408
- """Decide whether a command builds a verdict path with obfuscation and writes.
409
-
410
- A character-construction primitive (``chr(``, ``bytes.fromhex(``, base64
411
- decode, ``codecs.decode(``, a ``bytes([...])``/``bytearray([...])``
412
- int-list constructor) assembles a path from codes that carry no
413
- literal ``verification`` or ``.claude`` substring, so the text-pattern
414
- matchers miss it. The pairing fires only when three conditions hold
415
- together: an obfuscation primitive, a non-redirect write call (``open(``,
416
- ``write_text``, ``Out-File``, ``Set-Content``, ``Add-Content``, ``tee``),
417
- and a verdict-path segment reachable by that write — either a decodable
418
- token that decodes to a segment body anywhere in the command, or a
419
- plain-text segment name inside the write call's argument region.
420
-
421
- Scoping the plain-text segment to the write region leaves an unrelated
422
- decode-and-print one-liner whose write targets another path
423
- (``open(chr(...)+chr(...)); print('verification done')``) alone, because
424
- the incidental ``verification`` sits in a later statement rather than the
425
- write call. A decode-to-file one-liner with no segment
426
- (``open('decoded.bin','wb').write(b64decode(x))``) is also left alone,
427
- while a forge whose path decodes to the verdict directory is still caught.
428
- A bare ``>`` redirect to the verdict path is caught by the literal-path
429
- matchers, so a command whose only write is a redirect is left alone here.
430
-
431
- Args:
432
- command_text: The raw command string from the tool payload.
433
-
434
- Returns:
435
- True when the command pairs a path-obfuscation primitive with a
436
- non-redirect file write and a verdict-path segment reachable by that
437
- write.
438
- """
439
- has_obfuscation_primitive = re.search(PATH_OBFUSCATION_PRIMITIVE_PATTERN, command_text) is not None
440
- has_non_redirect_write = (
441
- re.search(NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN, command_text) is not None
442
- )
443
- if not (has_obfuscation_primitive and has_non_redirect_write):
444
- return False
445
- if _decoded_token_references_verdict_segment(command_text):
446
- return True
447
- return _write_call_region_names_verdict_segment(command_text)
448
-
449
-
450
- def _segments_join_as_verdict_path(command_text: str) -> bool:
451
- """Decide whether the two name segments sit adjacent in a path-join shape.
452
-
453
- The ``.claude`` home name and the ``verification`` directory name connect
454
- through only path-join glue — quotes, a ``+`` concatenation operator,
455
- whitespace, and a path separator — in either order, so a string
456
- concatenation (``'/.claude'+'/verification'``) reads as one verdict path
457
- while a prose mention (``.claude docs about the verification flow``) does
458
- not, because the words between the segments are not path-join glue.
459
-
460
- Args:
461
- command_text: The raw command string from the tool payload.
462
-
463
- Returns:
464
- True when the two segments connect through a path-join shape.
465
- """
466
- home_name = re.escape(CLAUDE_HOME_DIRECTORY_NAME)
467
- verdict_name = re.escape(VERDICT_DIRECTORY_NAME)
468
- path_join_pattern = re.compile(
469
- home_name + VERDICT_PATH_GLUE_PATTERN + verdict_name
470
- + "|"
471
- + verdict_name + VERDICT_PATH_GLUE_PATTERN + home_name,
472
- re.IGNORECASE,
473
- )
474
- return path_join_pattern.search(command_text) is not None
475
-
476
-
477
- def _segments_join_through_shell_variable(command_text: str) -> bool:
478
- """Decide whether a shell variable path-joins the two name segments.
479
-
480
- One segment binds to a shell variable (``p=~/.claude``,
481
- ``VDIR=verification``) whose ``$p``/``$VDIR`` reference is then
482
- path-joined toward the verdict directory (``$p/verification``,
483
- ``$VDIR/a.json``); the other segment appears elsewhere in the command. A
484
- redirect to ``/tmp/notes.txt`` carries no path-joined variable reference,
485
- so a benign mention of both words passes.
486
-
487
- Args:
488
- command_text: The raw command string from the tool payload.
489
-
490
- Returns:
491
- True when a path-joined variable reference binds one segment while the
492
- other segment appears in the command.
493
- """
494
- lowercased_command = command_text.lower()
495
- home_name = CLAUDE_HOME_DIRECTORY_NAME.lower()
496
- verdict_name = VERDICT_DIRECTORY_NAME.lower()
497
- joined_variable_pattern = re.compile(VERDICT_PATH_JOINED_VARIABLE_PATTERN)
498
- for each_prefix_name, each_suffix_name in joined_variable_pattern.findall(command_text):
499
- variable_name = each_prefix_name or each_suffix_name
500
- assignment_pattern = re.compile(
501
- VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN % re.escape(variable_name)
502
- )
503
- assignment_match = assignment_pattern.search(command_text)
504
- if assignment_match is None:
505
- continue
506
- assigned_value = assignment_match.group(1).lower()
507
- binds_home = home_name in assigned_value
508
- binds_verdict = verdict_name in assigned_value
509
- if binds_home and verdict_name in lowercased_command:
510
- return True
511
- if binds_verdict and home_name in lowercased_command:
512
- return True
513
- return False
514
-
515
-
516
- def _changes_through_split_directory_change_into_verdict(command_text: str) -> bool:
517
- """Decide whether a split directory change reaches the verdict directory then runs a command.
518
-
519
- A change into the Claude home (``cd ~/.claude``) followed by a change into
520
- a relative ``verification`` directory (``cd verification``) lands in the
521
- verdict directory without naming the two segments adjacently. The trust
522
- contract denies every shell command that reaches the verdict directory, so
523
- any command after the second change — a redirect, ``cp``, ``mv``, ``tee``,
524
- ``install``, or a ``python -c`` write — is a forge vector, mirroring the
525
- single-step ``cd ~/.claude/verification`` case. An unrelated second change
526
- (``cd hooks``) lands elsewhere and passes.
527
-
528
- Args:
529
- command_text: The raw command string from the tool payload.
530
-
531
- Returns:
532
- True when a change into the Claude home precedes a change into a
533
- relative verdict directory that is itself followed by any further
534
- command.
535
- """
536
- change_into_claude_pattern = re.compile(
537
- DIRECTORY_CHANGE_PATTERN_PREFIX
538
- + _directory_change_verbs_pattern()
539
- + DIRECTORY_CHANGE_PATTERN_SUFFIX
540
- + _directory_change_option_prefix_pattern()
541
- + DIRECTORY_CHANGE_TARGET_PATTERN
542
- + re.escape(CLAUDE_HOME_DIRECTORY_NAME)
543
- + CLAUDE_HOME_TARGET_BOUNDARY_PATTERN,
544
- re.IGNORECASE,
545
- )
546
- change_into_claude_match = change_into_claude_pattern.search(command_text)
547
- if change_into_claude_match is None:
548
- return False
549
- change_into_verdict_pattern = re.compile(
550
- DIRECTORY_CHANGE_PATTERN_PREFIX
551
- + _directory_change_verbs_pattern()
552
- + DIRECTORY_CHANGE_PATTERN_SUFFIX
553
- + _directory_change_option_prefix_pattern()
554
- + VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN,
555
- re.IGNORECASE,
556
- )
557
- change_into_verdict_match = change_into_verdict_pattern.search(
558
- command_text, change_into_claude_match.end()
559
- )
560
- if change_into_verdict_match is None:
561
- return False
562
- command_after_change_pattern = re.compile(COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN)
563
- return (
564
- command_after_change_pattern.search(command_text, change_into_verdict_match.end())
565
- is not None
566
- )
567
-
568
-
569
- def _writes_with_verdict_path_intent(command_text: str) -> bool:
570
- """Decide whether a write builds a verdict path from joined segments.
571
-
572
- A path split across string concatenation (``'/.claude'+'/verification'``)
573
- or a shell variable (``$p/verification``, ``$VDIR/a.json``) lands in the
574
- verdict directory while breaking the adjacency the literal-path matchers
575
- require, yet the two name segments still connect through a path-join shape.
576
- The literal-concatenation shape is paired with a non-redirect write
577
- (``open(``, ``write_text``, ``Out-File``, ``Set-Content``, ``Add-Content``,
578
- ``tee``): a bare ``>`` redirect to an adjacent ``.claude/verification``
579
- literal is already caught by the absolute-path matcher, so requiring a
580
- non-redirect write here leaves a commit message that merely quotes the
581
- path before a redirect (``git commit -m "fix .claude/verification gate"
582
- > /tmp/commit.log``) alone. The shell-variable shape is paired with any
583
- write primitive, including a redirect, because ``echo x > $p/verification``
584
- is itself the forge. A command whose two name segments only co-occur as
585
- free prose inside a quoted message
586
- (``echo "updated .claude docs about the verification flow"
587
- > /tmp/notes.txt``) carries no path-join shape and passes, as does a plain
588
- ``git commit -m "fix .claude/verification gate"`` with no write primitive.
589
-
590
- Args:
591
- command_text: The raw command string from the tool payload.
592
-
593
- Returns:
594
- True when a non-redirect write co-occurs with a literal concatenation
595
- of the two verdict-path name segments, or when any write co-occurs
596
- with a shell variable that path-joins the two segments.
597
- """
598
- has_non_redirect_write = (
599
- re.search(NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN, command_text) is not None
600
- )
601
- if has_non_redirect_write and _segments_join_as_verdict_path(command_text):
602
- return True
603
- has_write_primitive = re.search(FILE_WRITE_PRIMITIVE_PATTERN, command_text) is not None
604
- if not has_write_primitive:
605
- return False
606
- return _segments_join_through_shell_variable(command_text)
607
-
608
-
609
- def references_verdict_directory(command_text: str) -> bool:
610
- """Decide whether a command references the verification verdict directory.
611
-
612
- Args:
613
- command_text: The raw command string from the tool payload.
614
-
615
- Returns:
616
- True when the command names an absolute verdict path, changes into
617
- the Claude home before a relative verdict write, changes directly
618
- into the verdict directory before any command, steps through a split
619
- directory change into the verdict directory before any command, names
620
- a verdict file's ``verification/<root-key>.json`` shape, pairs a
621
- path-obfuscation primitive with a non-redirect file write, or pairs a
622
- file write with a path-join shape connecting both verdict-path name
623
- segments.
624
- """
625
- if _references_absolute_verdict_path(command_text):
626
- return True
627
- if _changes_into_claude_home_then_writes_relative(command_text):
628
- return True
629
- if _changes_into_verdict_directory_then_writes(command_text):
630
- return True
631
- if _changes_through_split_directory_change_into_verdict(command_text):
632
- return True
633
- if _references_verdict_file_shape(command_text):
634
- return True
635
- if _writes_with_verdict_path_intent(command_text):
636
- return True
637
- return _uses_obfuscated_path_write(command_text)
638
-
639
-
640
- def decision_for_payload(pretooluse_payload: dict) -> dict | None:
641
- """Build the deny decision for a verdict-directory shell access.
642
-
643
- Args:
644
- pretooluse_payload: The PreToolUse hook payload.
645
-
646
- Returns:
647
- The deny decision mapping when a gated shell command references the
648
- verdict directory; None when the command may proceed.
649
- """
650
- if pretooluse_payload.get("tool_name", "") not in ALL_GATED_TOOL_NAMES:
651
- return None
652
- command_text = pretooluse_payload.get("tool_input", {}).get("command", "")
653
- if not command_text:
654
- return None
655
- if not references_verdict_directory(command_text):
656
- return None
657
- return {
658
- "hookSpecificOutput": {
659
- "hookEventName": "PreToolUse",
660
- "permissionDecision": "deny",
661
- "permissionDecisionReason": VERDICT_DIRECTORY_GUARD_MESSAGE,
662
- }
663
- }
664
-
665
-
666
- def main() -> None:
667
- """Read the PreToolUse payload and deny verdict-directory shell access."""
668
- pretooluse_payload = read_hook_input_dictionary_from_stdin()
669
- if pretooluse_payload is None:
670
- return
671
- deny_decision = decision_for_payload(pretooluse_payload)
672
- if deny_decision is None:
673
- return
674
- raw_tool_name = pretooluse_payload.get("tool_name", "")
675
- tool_name_for_log = raw_tool_name if isinstance(raw_tool_name, str) else ""
676
- log_hook_block(
677
- calling_hook_name="verdict_directory_write_blocker.py",
678
- hook_event="PreToolUse",
679
- block_reason=VERDICT_DIRECTORY_GUARD_MESSAGE,
680
- tool_name=tool_name_for_log,
681
- )
682
- print(json.dumps(deny_decision))
683
- sys.stdout.flush()
684
-
685
-
686
- if __name__ == "__main__":
687
- main()