claude-dev-env 2.3.0 → 2.5.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +3 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +3 -4
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -0,0 +1,233 @@
1
+ """Read and write the per-work-tree code-review stamp files.
2
+
3
+ A stamp records that a clean ``/code-review`` pass ran against an exact branch
4
+ surface at a given effort. Each work tree keeps one JSON file under
5
+ ``~/.claude/code-review-stamps/`` mapping an effort token to the surface hash it
6
+ was earned against, so a later push or pull-request gate can ask whether a
7
+ clean review at the needed effort already covers the live surface. This module
8
+ only reads and writes those files; it runs no review.
9
+
10
+ ::
11
+
12
+ record_clean_stamp(root, hash_a, "low")
13
+ stamp_covers_surface(root, hash_a, "low") -> True (recorded, matching)
14
+ stamp_covers_surface(root, hash_a, "xhigh") -> False (low is below xhigh)
15
+ stamp_covers_surface(root, hash_b, "low") -> False (surface hash differs)
16
+
17
+ Coverage fails closed: a missing, unreadable, or malformed stamp file, a hash
18
+ that does not match, and an effort below the threshold all read as not covered.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import sys
25
+ import time
26
+ from pathlib import Path
27
+
28
+ _blocking_directory = str(Path(__file__).resolve().parent)
29
+
30
+ try:
31
+ from code_review_enforcement_config_bootstrap import (
32
+ register_code_review_enforcement_constants,
33
+ )
34
+ from verified_commit_config_bootstrap import register_verified_commit_constants
35
+ except ModuleNotFoundError:
36
+ if _blocking_directory not in sys.path:
37
+ sys.path.insert(0, _blocking_directory)
38
+ from code_review_enforcement_config_bootstrap import (
39
+ register_code_review_enforcement_constants,
40
+ )
41
+ from verified_commit_config_bootstrap import register_verified_commit_constants
42
+
43
+ register_code_review_enforcement_constants()
44
+ register_verified_commit_constants()
45
+
46
+ try:
47
+ from config.code_review_enforcement_constants import (
48
+ STAMP_DIRECTORY_NAME,
49
+ STAMP_KEY_EFFORT,
50
+ STAMP_KEY_MANIFEST_SHA256,
51
+ STAMP_KEY_RECORDED_AT_EPOCH,
52
+ effort_meets_threshold,
53
+ )
54
+ from config.verified_commit_constants import (
55
+ CLAUDE_HOME_DIRECTORY_NAME,
56
+ VERDICT_JSON_INDENT,
57
+ )
58
+ from verification_verdict_store import (
59
+ branch_surface_manifest,
60
+ empty_surface_hash,
61
+ manifest_sha256,
62
+ resolve_merge_base,
63
+ resolve_repo_root,
64
+ root_key_for_repo,
65
+ )
66
+ except ModuleNotFoundError:
67
+ if _blocking_directory not in sys.path:
68
+ sys.path.insert(0, _blocking_directory)
69
+ from config.code_review_enforcement_constants import (
70
+ STAMP_DIRECTORY_NAME,
71
+ STAMP_KEY_EFFORT,
72
+ STAMP_KEY_MANIFEST_SHA256,
73
+ STAMP_KEY_RECORDED_AT_EPOCH,
74
+ effort_meets_threshold,
75
+ )
76
+ from config.verified_commit_constants import (
77
+ CLAUDE_HOME_DIRECTORY_NAME,
78
+ VERDICT_JSON_INDENT,
79
+ )
80
+ from verification_verdict_store import (
81
+ branch_surface_manifest,
82
+ empty_surface_hash,
83
+ manifest_sha256,
84
+ resolve_merge_base,
85
+ resolve_repo_root,
86
+ root_key_for_repo,
87
+ )
88
+
89
+
90
+ def stamp_directory() -> Path:
91
+ """Return the shared directory holding every work tree's stamp file.
92
+
93
+ Stamps live outside any repository (under the user's Claude home) so no repo
94
+ accumulates untracked stamp files; every work tree's stamp shares this one
95
+ directory, distinguished by file name.
96
+
97
+ Returns:
98
+ The stamp directory under the user's Claude home.
99
+ """
100
+ return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / STAMP_DIRECTORY_NAME
101
+
102
+
103
+ def stamp_path_for_repo(repo_root: str) -> Path:
104
+ """Derive the stamp file path for a repository work tree.
105
+
106
+ Keyed by the shared ``root_key_for_repo`` derivation, so every work tree
107
+ gets its own stamp file and a subdirectory of a work tree resolves to the
108
+ same key as its root.
109
+
110
+ Args:
111
+ repo_root: The repository top-level directory.
112
+
113
+ Returns:
114
+ The stamp file path for this work tree.
115
+ """
116
+ return stamp_directory() / f"{root_key_for_repo(repo_root)}.json"
117
+
118
+
119
+ def _read_stamps_by_effort(stamp_file: Path) -> dict[str, object]:
120
+ """Read the per-effort stamp records for a work tree, or an empty map.
121
+
122
+ ::
123
+
124
+ <root-key>.json holds {"low": {...}, "xhigh": {...}} -> that map
125
+ file missing / unreadable / not a JSON object -> {}
126
+
127
+ A missing, unreadable, or malformed stamp file yields an empty map, so a
128
+ caller reading it sees no coverage and fails closed.
129
+
130
+ Args:
131
+ stamp_file: The per-work-tree stamp file path.
132
+
133
+ Returns:
134
+ The mapping of effort token to its stored record, or an empty map when
135
+ the file is absent, unreadable, or not a JSON object.
136
+ """
137
+ try:
138
+ parsed_stamps = json.loads(stamp_file.read_text(encoding="utf-8"))
139
+ except (OSError, json.JSONDecodeError):
140
+ return {}
141
+ if not isinstance(parsed_stamps, dict):
142
+ return {}
143
+ return parsed_stamps
144
+
145
+
146
+ def record_clean_stamp(repo_root: str, bound_manifest_sha256: str, effort: str) -> Path:
147
+ """Write one effort's clean-review stamp for a work tree.
148
+
149
+ Reads any existing per-effort records, sets this effort's entry to the given
150
+ surface hash and the current epoch, and writes the map back. Recording a
151
+ second effort keeps the first; recording the same effort again overwrites
152
+ its bound hash.
153
+
154
+ Args:
155
+ repo_root: The repository top-level directory.
156
+ bound_manifest_sha256: Hash of the branch surface the clean review covered.
157
+ effort: The effort token the clean review ran at.
158
+
159
+ Returns:
160
+ The stamp file path the record was written to.
161
+ """
162
+ stamp_file = stamp_path_for_repo(repo_root)
163
+ stamp_file.parent.mkdir(parents=True, exist_ok=True)
164
+ all_stamps_by_effort = _read_stamps_by_effort(stamp_file)
165
+ all_stamps_by_effort[effort] = {
166
+ STAMP_KEY_EFFORT: effort,
167
+ STAMP_KEY_MANIFEST_SHA256: bound_manifest_sha256,
168
+ STAMP_KEY_RECORDED_AT_EPOCH: int(time.time()),
169
+ }
170
+ stamp_file.write_text(
171
+ json.dumps(all_stamps_by_effort, indent=VERDICT_JSON_INDENT), encoding="utf-8"
172
+ )
173
+ return stamp_file
174
+
175
+
176
+ def stamp_covers_surface(repo_root: str, live_manifest_sha256: str, required_effort: str) -> bool:
177
+ """Decide whether a clean stamp covers the live surface at the needed effort.
178
+
179
+ A stored entry covers the surface when its bound hash equals the live hash
180
+ exactly and its effort ranks at or above the required effort. Any missing,
181
+ unreadable, or malformed stamp reads as not covered.
182
+
183
+ Args:
184
+ repo_root: The repository top-level directory.
185
+ live_manifest_sha256: Hash of the live branch surface the gate checks.
186
+ required_effort: The effort the gate demands for the action.
187
+
188
+ Returns:
189
+ True as soon as one stored entry matches the live hash and meets the
190
+ required effort; False when none match.
191
+ """
192
+ all_stamps_by_effort = _read_stamps_by_effort(stamp_path_for_repo(repo_root))
193
+ for each_stamp_record in all_stamps_by_effort.values():
194
+ if not isinstance(each_stamp_record, dict):
195
+ continue
196
+ if each_stamp_record.get(STAMP_KEY_MANIFEST_SHA256) != live_manifest_sha256:
197
+ continue
198
+ stored_effort = each_stamp_record.get(STAMP_KEY_EFFORT)
199
+ if not isinstance(stored_effort, str):
200
+ continue
201
+ if effort_meets_threshold(stored_effort, required_effort):
202
+ return True
203
+ return False
204
+
205
+
206
+ def live_surface_hash(work_tree_directory: str) -> str | None:
207
+ """Compute the live branch-surface manifest hash for a work tree.
208
+
209
+ Resolves the work tree's repo root and merge base, then hashes the current
210
+ change surface — every changed path and untracked file bound by its content
211
+ digest — so the hash tracks the exact bytes under review.
212
+
213
+ Args:
214
+ work_tree_directory: A directory inside the work tree to hash.
215
+
216
+ Returns:
217
+ The surface manifest hash, or None when the directory is outside a work
218
+ tree, the merge base or manifest cannot be resolved, or the change
219
+ surface is empty (HEAD equals the merge base, so nothing is under review).
220
+ """
221
+ repo_root = resolve_repo_root(work_tree_directory)
222
+ if repo_root is None:
223
+ return None
224
+ merge_base_sha = resolve_merge_base(repo_root)
225
+ if merge_base_sha is None:
226
+ return None
227
+ surface_manifest_text = branch_surface_manifest(repo_root, merge_base_sha)
228
+ if surface_manifest_text is None:
229
+ return None
230
+ surface_hash = manifest_sha256(surface_manifest_text)
231
+ if surface_hash == empty_surface_hash():
232
+ return None
233
+ return surface_hash
@@ -0,0 +1,7 @@
1
+ """Concern modules of the code-review stamp write blocker, wired by the entry hook.
2
+
3
+ The entry hook ``code_review_stamp_directory_write_blocker.py`` imports two
4
+ public matchers from this package to keep its own file short: a split
5
+ directory-change matcher and an obfuscated-path-write matcher. Each mirrors the
6
+ sibling verdict-directory guard's logic against the code-review stamp segments.
7
+ """
@@ -0,0 +1,15 @@
1
+ """Put the blocking and hooks directories on sys.path for the parts tests.
2
+
3
+ The concern modules import their shared constants as ``from config...`` against
4
+ the sibling ``blocking/config/`` package, so the tests need the blocking
5
+ directory (which holds that package) and the hooks directory on the path.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ _blocking_directory = str(Path(__file__).resolve().parents[1])
12
+ _hooks_directory = str(Path(__file__).resolve().parents[2])
13
+ for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
14
+ if each_bootstrap_directory not in sys.path:
15
+ sys.path.insert(0, each_bootstrap_directory)
@@ -0,0 +1,212 @@
1
+ """Decide whether a command builds a stamp path with obfuscation and writes.
2
+
3
+ A path-obfuscation forge hides ``.claude`` or ``code-review-stamps`` inside hex
4
+ digits, a base64 token, a comma-separated character-code sequence, or a run of
5
+ ``+``-joined ``chr(<int>)`` calls, then pairs the assembled path with a
6
+ non-redirect file write (``open(``, ``write_text``, ``Out-File``,
7
+ ``Set-Content``, ``Add-Content``, ``tee``). The literal path matchers miss such
8
+ a path because it carries no plain ``code-review-stamps`` substring, so this
9
+ module decodes each embedded token and looks for a stamp-path segment either in
10
+ the decoded text or inside the write call's argument region. A bare ``>``
11
+ redirect to the stamp path is caught by the literal-path matchers, so a command
12
+ whose only write is a redirect is left alone here.
13
+
14
+ The entry hook registers the constant packages before importing this module, so
15
+ the ``from config...`` imports below resolve against the sibling ``config/``
16
+ package without a bootstrap call here.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ import binascii
23
+ import re
24
+
25
+ from config.code_review_enforcement_constants import (
26
+ ALL_STAMP_PATH_SEGMENT_BODIES,
27
+ ALL_STAMP_PATH_SEGMENT_NAMES,
28
+ )
29
+ from config.verified_commit_constants import (
30
+ BASE64_TOKEN_PATTERN,
31
+ CHARACTER_CODE_SEQUENCE_PATTERN,
32
+ CHR_CALL_CHAIN_PATTERN,
33
+ CHR_CALL_CODE_PATTERN,
34
+ HEX_DIGITS_PER_BYTE,
35
+ HEX_TOKEN_PATTERN,
36
+ NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN,
37
+ PATH_OBFUSCATION_PRIMITIVE_PATTERN,
38
+ WRITE_CALL_REGION_PATTERN,
39
+ )
40
+
41
+ _ASCII_ENCODING = "ascii"
42
+
43
+
44
+ def _decode_hex_token(hex_token: str) -> str:
45
+ """Decode a hex-digit token to text, returning empty text on failure.
46
+
47
+ Args:
48
+ hex_token: A run of hex digits taken from the command.
49
+
50
+ Returns:
51
+ The decoded ASCII text, or empty text when the token is odd-length,
52
+ not valid hex, or not decodable as ASCII.
53
+ """
54
+ odd_hex_token_remainder = 1
55
+ if len(hex_token) % HEX_DIGITS_PER_BYTE == odd_hex_token_remainder:
56
+ return ""
57
+ try:
58
+ return bytes.fromhex(hex_token).decode(_ASCII_ENCODING)
59
+ except (ValueError, binascii.Error):
60
+ return ""
61
+
62
+
63
+ def _decode_base64_token(base64_token: str) -> str:
64
+ """Decode a base64 token to text, returning empty text on failure.
65
+
66
+ Args:
67
+ base64_token: A base64-shaped token taken from the command.
68
+
69
+ Returns:
70
+ The decoded ASCII text, or empty text when the token is not valid
71
+ base64 or not decodable as ASCII.
72
+ """
73
+ try:
74
+ return base64.b64decode(base64_token, validate=True).decode(_ASCII_ENCODING)
75
+ except (ValueError, binascii.Error):
76
+ return ""
77
+
78
+
79
+ def _decode_character_code_sequence(code_sequence: str) -> str:
80
+ """Decode a comma-separated character-code sequence to text.
81
+
82
+ Args:
83
+ code_sequence: A comma-separated run of decimal character codes, each
84
+ within the three-digit range the source pattern admits.
85
+
86
+ Returns:
87
+ The decoded text.
88
+ """
89
+ character_code_separator = ","
90
+ decoded_characters: list[str] = []
91
+ for each_code in code_sequence.split(character_code_separator):
92
+ code_point = int(each_code.strip())
93
+ decoded_characters.append(chr(code_point))
94
+ return "".join(decoded_characters)
95
+
96
+
97
+ def _decode_chr_call_chain(chr_chain: str) -> str:
98
+ """Decode a run of ``+``-joined ``chr(<int>)`` calls to text.
99
+
100
+ Args:
101
+ chr_chain: A run of two or more ``+``-joined ``chr(<int>)`` calls taken
102
+ from the command, each code within the three-digit source range.
103
+
104
+ Returns:
105
+ The decoded text.
106
+ """
107
+ decoded_characters: list[str] = []
108
+ for each_code in re.findall(CHR_CALL_CODE_PATTERN, chr_chain, re.IGNORECASE):
109
+ decoded_characters.append(chr(int(each_code)))
110
+ return "".join(decoded_characters)
111
+
112
+
113
+ def _append_when_present(all_decoded_texts: list[str], decoded_text: str) -> None:
114
+ """Append the lowercase decoded text when it is non-empty.
115
+
116
+ Args:
117
+ all_decoded_texts: The running list of decoded texts to extend.
118
+ decoded_text: One token's decoded text, empty when the token failed.
119
+
120
+ Returns:
121
+ None. The list is extended in place when the text is non-empty.
122
+ """
123
+ if decoded_text:
124
+ all_decoded_texts.append(decoded_text.lower())
125
+
126
+
127
+ def _decoded_texts_from_command(command_text: str) -> list[str]:
128
+ """Decode every hex, base64, character-code, and chr-chain token a command embeds.
129
+
130
+ Args:
131
+ command_text: The raw command string from the tool payload.
132
+
133
+ Returns:
134
+ The lowercase decoded text for every token that decodes cleanly;
135
+ tokens that fail to decode or carry an out-of-range code are skipped.
136
+ """
137
+ all_decoded_texts: list[str] = []
138
+ for each_hex_token in re.findall(HEX_TOKEN_PATTERN, command_text, re.IGNORECASE):
139
+ _append_when_present(all_decoded_texts, _decode_hex_token(each_hex_token))
140
+ for each_base64_token in re.findall(BASE64_TOKEN_PATTERN, command_text):
141
+ _append_when_present(all_decoded_texts, _decode_base64_token(each_base64_token))
142
+ for each_code_sequence in re.findall(CHARACTER_CODE_SEQUENCE_PATTERN, command_text):
143
+ _append_when_present(all_decoded_texts, _decode_character_code_sequence(each_code_sequence))
144
+ for each_chr_chain in re.findall(CHR_CALL_CHAIN_PATTERN, command_text, re.IGNORECASE):
145
+ _append_when_present(all_decoded_texts, _decode_chr_call_chain(each_chr_chain))
146
+ return all_decoded_texts
147
+
148
+
149
+ def _decoded_token_references_stamp_segment(command_text: str) -> bool:
150
+ """Decide whether a decodable token in a command hides a stamp-path segment.
151
+
152
+ Args:
153
+ command_text: The raw command string from the tool payload.
154
+
155
+ Returns:
156
+ True when a decodable token decodes to a stamp-path segment body
157
+ (``claude`` or ``code-review-stamps``).
158
+ """
159
+ all_decoded_texts = _decoded_texts_from_command(command_text)
160
+ return any(
161
+ each_segment_body in each_decoded_text
162
+ for each_decoded_text in all_decoded_texts
163
+ for each_segment_body in ALL_STAMP_PATH_SEGMENT_BODIES
164
+ )
165
+
166
+
167
+ def _write_call_region_names_stamp_segment(command_text: str) -> bool:
168
+ """Decide whether a write call's argument region names a stamp-path segment.
169
+
170
+ Args:
171
+ command_text: The raw command string from the tool payload.
172
+
173
+ Returns:
174
+ True when a stamp-path segment name appears inside a non-redirect write
175
+ call's argument region.
176
+ """
177
+ write_call_region_pattern = re.compile(WRITE_CALL_REGION_PATTERN, re.IGNORECASE)
178
+ for each_region_match in write_call_region_pattern.finditer(command_text):
179
+ lowercased_region = each_region_match.group(0).lower()
180
+ if any(each_name in lowercased_region for each_name in ALL_STAMP_PATH_SEGMENT_NAMES):
181
+ return True
182
+ return False
183
+
184
+
185
+ def references_obfuscated_stamp_path(command_text: str) -> bool:
186
+ """Decide whether a command pairs path obfuscation with a stamp-path write.
187
+
188
+ ::
189
+
190
+ python -c "open(bytes.fromhex('...code-review-stamps...'),'w')" -> True
191
+ open(bytes.fromhex('2f746d702f78'),'w') -> False
192
+
193
+ Args:
194
+ command_text: The raw command string from the tool payload.
195
+
196
+ Returns:
197
+ True when the command holds a path-obfuscation primitive, a
198
+ non-redirect file write, and a stamp-path segment reachable by that
199
+ write — either a decoded token anywhere in the command or a plain-text
200
+ segment inside the write call's argument region.
201
+ """
202
+ has_obfuscation_primitive = (
203
+ re.search(PATH_OBFUSCATION_PRIMITIVE_PATTERN, command_text) is not None
204
+ )
205
+ has_non_redirect_write = (
206
+ re.search(NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN, command_text) is not None
207
+ )
208
+ if not (has_obfuscation_primitive and has_non_redirect_write):
209
+ return False
210
+ if _decoded_token_references_stamp_segment(command_text):
211
+ return True
212
+ return _write_call_region_names_stamp_segment(command_text)
@@ -0,0 +1,138 @@
1
+ """Decide whether a split directory change reaches the stamp directory then runs a command.
2
+
3
+ A change into the Claude home (``cd ~/.claude``) followed by a change into a
4
+ relative ``code-review-stamps`` directory (``cd code-review-stamps``) lands in
5
+ the stamp directory without ever naming the two segments adjacently, so the
6
+ absolute-path matcher and the single-step ``cd ~/.claude/code-review-stamps``
7
+ matcher both miss it. The trust contract denies every shell command that
8
+ reaches the stamp directory, so any command after the second change — a
9
+ redirect, ``cp``, ``mv``, ``tee``, ``install``, or a ``python -c`` write — is a
10
+ forge vector. An unrelated second change (``cd hooks``) lands elsewhere and
11
+ passes.
12
+
13
+ The entry hook registers the constant packages before importing this module, so
14
+ the ``from config...`` imports below resolve against the sibling ``config/``
15
+ package without a bootstrap call here.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+
22
+ from config.code_review_enforcement_constants import STAMP_DIRECTORY_CHANGE_TARGET_PATTERN
23
+ from config.verified_commit_constants import (
24
+ CLAUDE_HOME_DIRECTORY_NAME,
25
+ CLAUDE_HOME_TARGET_BOUNDARY_PATTERN,
26
+ COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN,
27
+ DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN,
28
+ DIRECTORY_CHANGE_OPTION_TERMINATOR,
29
+ DIRECTORY_CHANGE_PATH_OPTIONS,
30
+ DIRECTORY_CHANGE_PATTERN_PREFIX,
31
+ DIRECTORY_CHANGE_PATTERN_SUFFIX,
32
+ DIRECTORY_CHANGE_TARGET_PATTERN,
33
+ DIRECTORY_CHANGE_VERBS,
34
+ )
35
+ from config.verified_commit_gate_output_constants import REGEX_ALTERNATION_SEPARATOR
36
+
37
+
38
+ def _directory_change_verbs_pattern() -> str:
39
+ """Build the alternation of directory-change verbs for a change matcher.
40
+
41
+ Returns:
42
+ The regex alternation of escaped directory-change verbs in sorted order.
43
+ """
44
+ return REGEX_ALTERNATION_SEPARATOR.join(
45
+ re.escape(each_verb) for each_verb in sorted(DIRECTORY_CHANGE_VERBS)
46
+ )
47
+
48
+
49
+ def _directory_change_option_prefix_pattern() -> str:
50
+ """Build the optional path-option prefix that may precede a change target.
51
+
52
+ Returns:
53
+ The regex matching zero or more leading path-option or terminator
54
+ tokens, ready to sit between the change suffix and target patterns.
55
+ """
56
+ option_alternation = REGEX_ALTERNATION_SEPARATOR.join(
57
+ re.escape(each_option)
58
+ for each_option in sorted(
59
+ DIRECTORY_CHANGE_PATH_OPTIONS | {DIRECTORY_CHANGE_OPTION_TERMINATOR}
60
+ )
61
+ )
62
+ return DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN % option_alternation
63
+
64
+
65
+ def directory_change_prefix() -> str:
66
+ """Build the change-verb prefix shared by the directory-change matchers.
67
+
68
+ The stamp-directory write-blocker reuses this same prefix, so a single
69
+ builder feeds both this module's split-change matchers and the blocker's
70
+ single-step change matchers, and the two never drift.
71
+
72
+ Returns:
73
+ The regex prefix matching a directory-change verb and any leading
74
+ path-option tokens, ready for a target pattern to follow.
75
+ """
76
+ return (
77
+ DIRECTORY_CHANGE_PATTERN_PREFIX
78
+ + _directory_change_verbs_pattern()
79
+ + DIRECTORY_CHANGE_PATTERN_SUFFIX
80
+ + _directory_change_option_prefix_pattern()
81
+ )
82
+
83
+
84
+ def _change_into_claude_pattern() -> re.Pattern[str]:
85
+ """Build the compiled matcher for a directory change into the Claude home.
86
+
87
+ Returns:
88
+ The compiled, case-insensitive pattern matching a change verb into any
89
+ path ending in ``.claude``.
90
+ """
91
+ return re.compile(
92
+ directory_change_prefix()
93
+ + DIRECTORY_CHANGE_TARGET_PATTERN
94
+ + re.escape(CLAUDE_HOME_DIRECTORY_NAME)
95
+ + CLAUDE_HOME_TARGET_BOUNDARY_PATTERN,
96
+ re.IGNORECASE,
97
+ )
98
+
99
+
100
+ def _change_into_stamp_pattern() -> re.Pattern[str]:
101
+ """Build the compiled matcher for a directory change into the stamp directory.
102
+
103
+ Returns:
104
+ The compiled, case-insensitive pattern matching a change verb into a
105
+ relative ``code-review-stamps`` directory.
106
+ """
107
+ return re.compile(
108
+ directory_change_prefix() + STAMP_DIRECTORY_CHANGE_TARGET_PATTERN, re.IGNORECASE
109
+ )
110
+
111
+
112
+ def changes_through_split_directory_into_stamp(command_text: str) -> bool:
113
+ """Decide whether a split change reaches the stamp directory then runs a command.
114
+
115
+ ::
116
+
117
+ cd ~/.claude && cd code-review-stamps && echo x > f.json -> True
118
+ cd ~/.claude && cd hooks && echo x > f.json -> False
119
+
120
+ Args:
121
+ command_text: The raw command string from the tool payload.
122
+
123
+ Returns:
124
+ True when a change into the Claude home precedes a change into a
125
+ relative stamp directory that is itself followed by any further command.
126
+ """
127
+ change_into_claude_match = _change_into_claude_pattern().search(command_text)
128
+ if change_into_claude_match is None:
129
+ return False
130
+ change_into_stamp_match = _change_into_stamp_pattern().search(
131
+ command_text, change_into_claude_match.end()
132
+ )
133
+ if change_into_stamp_match is None:
134
+ return False
135
+ command_after_change_pattern = re.compile(COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN)
136
+ return (
137
+ command_after_change_pattern.search(command_text, change_into_stamp_match.end()) is not None
138
+ )
@@ -0,0 +1,49 @@
1
+ """Behavioral tests for the obfuscated-stamp-path matcher.
2
+
3
+ The matcher blocks a command that assembles a stamp path from hex, base64, or
4
+ ``chr(<int>)`` codes and pairs it with a non-redirect file write, while a
5
+ decode-to-other-path one-liner passes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from code_review_stamp_write_blocker_parts.obfuscated_stamp_path_reference import (
11
+ references_obfuscated_stamp_path,
12
+ )
13
+
14
+ STAMP_PATH_TEXT = "/.claude/code-review-stamps/a.json"
15
+ BENIGN_PATH_TEXT = "/tmp/x"
16
+
17
+
18
+ def _hex_forge_command(path_text: str) -> str:
19
+ return f"python -c \"open(bytes.fromhex('{path_text.encode().hex()}'),'w').write('x')\""
20
+
21
+
22
+ def _chr_chain_forge_command(path_text: str) -> str:
23
+ chr_chain = "+".join(f"chr({ord(each_character)})" for each_character in path_text)
24
+ return f"python -c \"open({chr_chain},'w').write('x')\""
25
+
26
+
27
+ def _character_code_forge_command(path_text: str) -> str:
28
+ comma_separated_codes = ",".join(str(ord(each_character)) for each_character in path_text)
29
+ return f"python -c \"open(bytes([{comma_separated_codes}]),'w').write('x')\""
30
+
31
+
32
+ def test_hex_assembled_stamp_path_write_is_blocked() -> None:
33
+ assert references_obfuscated_stamp_path(_hex_forge_command(STAMP_PATH_TEXT))
34
+
35
+
36
+ def test_character_code_assembled_stamp_path_write_is_blocked() -> None:
37
+ assert references_obfuscated_stamp_path(_character_code_forge_command(STAMP_PATH_TEXT))
38
+
39
+
40
+ def test_chr_chain_assembled_stamp_path_write_is_blocked() -> None:
41
+ assert references_obfuscated_stamp_path(_chr_chain_forge_command(STAMP_PATH_TEXT))
42
+
43
+
44
+ def test_decode_to_other_path_write_is_not_blocked() -> None:
45
+ assert not references_obfuscated_stamp_path(_hex_forge_command(BENIGN_PATH_TEXT))
46
+
47
+
48
+ def test_redirect_only_stamp_reference_is_left_to_literal_matchers() -> None:
49
+ assert not references_obfuscated_stamp_path("echo x > ~/.claude/code-review-stamps/a.json")