claude-dev-env 1.95.0 → 2.0.1

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 (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,260 @@
1
+ """Repository-resolution behavior of the PII commit gate (issue #65).
2
+
3
+ The gate scans the repository the command names, not the session working
4
+ directory, so::
5
+
6
+ git -C <healthy repo> commit -> scans <healthy repo> from any cwd
7
+ cd <repo> && git commit -> scans <repo>
8
+ git -C <repo> commit (cwd gone) -> still scans <repo>, never refuses for cwd
9
+ git -C <not-a-repo> commit -> refusal names <not-a-repo>
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ import pytest
20
+ from pii_prevention_blocker import evaluate, evaluate_bash_command
21
+
22
+ _HOOK_PATH = Path(__file__).resolve().parents[1] / "pii_prevention_blocker.py"
23
+ _ALLOW_SLUG = "AllowOwner/allow-repo"
24
+ _OTHER_SLUG = "OtherOwner/other-repo"
25
+
26
+
27
+ def _run_hook(payload: dict[str, object]) -> tuple[int, str]:
28
+ completed_process = subprocess.run(
29
+ [sys.executable, str(_HOOK_PATH)],
30
+ input=json.dumps(payload),
31
+ capture_output=True,
32
+ text=True,
33
+ timeout=30,
34
+ check=False,
35
+ )
36
+ return completed_process.returncode, completed_process.stdout
37
+
38
+
39
+ def _assembled_fixture_email() -> str:
40
+ return "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
41
+
42
+
43
+ def _assembled_unlisted_email() -> str:
44
+ return "other.person" + "@" + "different-co" + ".example" + ".io"
45
+
46
+
47
+ def _write_allowlist_identity(identity_path: Path, slug: str, value: str) -> None:
48
+ identity_path.write_text(
49
+ json.dumps({"pii_allowlisted_values": {slug: [value]}}),
50
+ encoding="utf-8",
51
+ )
52
+
53
+
54
+ def _init_repo_with_origin_and_staged_value(
55
+ repository_root: Path, origin_slug: str, staged_value: str
56
+ ) -> None:
57
+ repository_root.mkdir(parents=True, exist_ok=True)
58
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
59
+ subprocess.run(
60
+ ["git", "config", "user.email", "dev@example.com"],
61
+ cwd=repository_root,
62
+ check=True,
63
+ )
64
+ subprocess.run(
65
+ ["git", "config", "user.name", "Fixture Dev"], cwd=repository_root, check=True
66
+ )
67
+ subprocess.run(
68
+ ["git", "remote", "add", "origin", "https://github.com/" + origin_slug + ".git"],
69
+ cwd=repository_root,
70
+ check=True,
71
+ )
72
+ (repository_root / "notes.md").write_text(
73
+ "owner email " + staged_value + "\n", encoding="utf-8"
74
+ )
75
+ subprocess.run(["git", "add", "notes.md"], cwd=repository_root, check=True)
76
+
77
+
78
+ def _commit_payload(repository_root: Path) -> dict[str, object]:
79
+ return {
80
+ "tool_name": "Bash",
81
+ "tool_input": {"command": f"git -C {repository_root} commit -m x"},
82
+ }
83
+
84
+
85
+ def test_commit_subprocess_allows_allowlisted_value_in_the_allowlisted_repo(
86
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
87
+ ) -> None:
88
+ allowed_value = _assembled_fixture_email()
89
+ identity_path = tmp_path / "local-identity.json"
90
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
91
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
92
+ repository_root = tmp_path / "repo"
93
+ _init_repo_with_origin_and_staged_value(repository_root, _ALLOW_SLUG, allowed_value)
94
+ return_code, hook_stdout = _run_hook(_commit_payload(repository_root))
95
+ assert return_code == 0
96
+ assert hook_stdout.strip() == ""
97
+
98
+
99
+ def test_commit_subprocess_blocks_allowlisted_value_in_a_different_repo(
100
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
101
+ ) -> None:
102
+ allowed_value = _assembled_fixture_email()
103
+ identity_path = tmp_path / "local-identity.json"
104
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, allowed_value)
105
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
106
+ repository_root = tmp_path / "repo"
107
+ _init_repo_with_origin_and_staged_value(repository_root, _OTHER_SLUG, allowed_value)
108
+ _return_code, hook_stdout = _run_hook(_commit_payload(repository_root))
109
+ assert "email" in hook_stdout
110
+
111
+
112
+ def test_commit_blocks_an_unlisted_value_in_the_allowlisted_repo(
113
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
114
+ ) -> None:
115
+ identity_path = tmp_path / "local-identity.json"
116
+ _write_allowlist_identity(identity_path, _ALLOW_SLUG, _assembled_fixture_email())
117
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
118
+ repository_root = tmp_path / "repo"
119
+ _init_repo_with_origin_and_staged_value(
120
+ repository_root, _ALLOW_SLUG, _assembled_unlisted_email()
121
+ )
122
+ deny_reason = evaluate_bash_command(
123
+ f"git -C {repository_root} commit -m x",
124
+ working_directory=str(repository_root),
125
+ )
126
+ assert deny_reason is not None
127
+ assert "email" in deny_reason
128
+
129
+
130
+ def test_commit_with_missing_local_identity_still_blocks_the_value(
131
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
132
+ ) -> None:
133
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(tmp_path / "absent.json"))
134
+ repository_root = tmp_path / "repo"
135
+ _init_repo_with_origin_and_staged_value(
136
+ repository_root, _ALLOW_SLUG, _assembled_fixture_email()
137
+ )
138
+ deny_reason = evaluate_bash_command(
139
+ f"git -C {repository_root} commit -m x",
140
+ working_directory=str(repository_root),
141
+ )
142
+ assert deny_reason is not None
143
+ assert "email" in deny_reason
144
+
145
+
146
+ def _init_repo_with_staged_pii(repository_root: Path) -> None:
147
+ repository_root.mkdir(parents=True, exist_ok=True)
148
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
149
+ subprocess.run(
150
+ ["git", "config", "user.email", "dev@example.com"],
151
+ cwd=repository_root,
152
+ check=True,
153
+ )
154
+ subprocess.run(
155
+ ["git", "config", "user.name", "Fixture Dev"],
156
+ cwd=repository_root,
157
+ check=True,
158
+ )
159
+ (repository_root / "notes.md").write_text(
160
+ "owner email " + _assembled_fixture_email() + "\n",
161
+ encoding="utf-8",
162
+ )
163
+ subprocess.run(["git", "add", "notes.md"], cwd=repository_root, check=True)
164
+
165
+
166
+ def test_dash_c_scans_named_repo_from_unrelated_cwd(tmp_path: Path) -> None:
167
+ repository_root = tmp_path / "healthy_repo"
168
+ _init_repo_with_staged_pii(repository_root)
169
+ deny_reason = evaluate_bash_command(
170
+ f"git -C {repository_root} commit -m x",
171
+ working_directory="/tmp",
172
+ )
173
+ assert deny_reason is not None
174
+ assert "email" in deny_reason
175
+
176
+
177
+ def test_cd_prefix_scans_named_repo_from_unrelated_cwd(tmp_path: Path) -> None:
178
+ repository_root = tmp_path / "healthy_repo"
179
+ _init_repo_with_staged_pii(repository_root)
180
+ deny_reason = evaluate_bash_command(
181
+ f"cd {repository_root} && git commit -m x",
182
+ working_directory="/tmp",
183
+ )
184
+ assert deny_reason is not None
185
+ assert "email" in deny_reason
186
+
187
+
188
+ def test_cd_prefix_with_git_exe_scans_named_repo_from_unrelated_cwd(
189
+ tmp_path: Path,
190
+ ) -> None:
191
+ repository_root = tmp_path / "healthy_repo"
192
+ _init_repo_with_staged_pii(repository_root)
193
+ deny_reason = evaluate_bash_command(
194
+ f"cd {repository_root} && git.exe commit -m x",
195
+ working_directory="/tmp",
196
+ )
197
+ assert deny_reason is not None
198
+ assert "email" in deny_reason
199
+
200
+
201
+ def test_removed_working_directory_does_not_block_named_repo(tmp_path: Path) -> None:
202
+ repository_root = tmp_path / "healthy_repo"
203
+ _init_repo_with_staged_pii(repository_root)
204
+ removed_directory = tmp_path / "gone"
205
+ removed_directory.mkdir()
206
+ removed_directory.rmdir()
207
+ deny_reason = evaluate_bash_command(
208
+ f"git -C {repository_root} commit -m x",
209
+ working_directory=str(removed_directory),
210
+ )
211
+ assert deny_reason is not None
212
+ assert "email" in deny_reason
213
+
214
+
215
+ def test_multiple_dash_c_compose_to_the_named_repo(tmp_path: Path) -> None:
216
+ repository_root = tmp_path / "parent" / "healthy_repo"
217
+ _init_repo_with_staged_pii(repository_root)
218
+ deny_reason = evaluate_bash_command(
219
+ f"git -C {tmp_path / 'parent'} -C healthy_repo commit -m x",
220
+ working_directory="/tmp",
221
+ )
222
+ assert deny_reason is not None
223
+ assert "email" in deny_reason
224
+
225
+
226
+ def test_full_dispatch_blocks_pii_commit_from_unrelated_cwd(tmp_path: Path) -> None:
227
+ repository_root = tmp_path / "healthy_repo"
228
+ _init_repo_with_staged_pii(repository_root)
229
+ deny_reason = evaluate(
230
+ {
231
+ "tool_name": "Bash",
232
+ "tool_input": {"command": f"git -C {repository_root} commit -m x"},
233
+ "cwd": "/tmp",
234
+ }
235
+ )
236
+ assert deny_reason is not None
237
+ assert "email" in deny_reason
238
+
239
+
240
+ def test_unresolvable_named_repo_refusal_names_the_path() -> None:
241
+ deny_reason = evaluate_bash_command(
242
+ "git -C /nonexistent/repo/xyz commit -m x",
243
+ working_directory="/tmp",
244
+ )
245
+ assert deny_reason is not None
246
+ assert "repository root" in deny_reason
247
+ assert "/nonexistent/repo/xyz" in deny_reason
248
+
249
+
250
+ def test_clean_named_repo_passes(tmp_path: Path) -> None:
251
+ repository_root = tmp_path / "clean_repo"
252
+ repository_root.mkdir()
253
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
254
+ (repository_root / "readme.md").write_text("all clear\n", encoding="utf-8")
255
+ subprocess.run(["git", "add", "readme.md"], cwd=repository_root, check=True)
256
+ deny_reason = evaluate_bash_command(
257
+ f"git -C {repository_root} commit -m x",
258
+ working_directory="/tmp",
259
+ )
260
+ assert deny_reason is None
@@ -0,0 +1,105 @@
1
+ """Behavioral tests for the moved repository-exemption helpers.
2
+
3
+ Covers the origin-URL slug parser and the per-repository exemption decision::
4
+
5
+ https://github.com/Owner/Repo.git -> owner/repo
6
+ https://evil.test/Owner/Repo.git -> None
7
+ ok: origin slug in CLAUDE_PII_EXEMPT_REPOS -> exempt (True)
8
+ flag: origin slug absent from the exempt set -> scanned (False)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+ from pii_prevention_blocker_parts.repository_exemption import (
19
+ _is_repository_exempt_from_pii_scan,
20
+ _owner_repo_slug_from_origin_url,
21
+ repository_allowlisted_values,
22
+ )
23
+
24
+ _ALLOWED_VALUE = "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
25
+
26
+
27
+ def _write_allowlist_identity(identity_path: Path, slug: str) -> None:
28
+ identity_path.write_text(
29
+ json.dumps({"pii_allowlisted_values": {slug: [_ALLOWED_VALUE]}}),
30
+ encoding="utf-8",
31
+ )
32
+
33
+
34
+ def test_repository_allowlisted_values_returns_the_mapped_values(
35
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
36
+ ) -> None:
37
+ identity_path = tmp_path / "local-identity.json"
38
+ _write_allowlist_identity(identity_path, "AllowOwner/allow-repo")
39
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
40
+ repository_root = tmp_path / "repo"
41
+ _init_repo_with_github_origin(repository_root, "AllowOwner/allow-repo")
42
+ assert repository_allowlisted_values(repository_root) == frozenset({_ALLOWED_VALUE})
43
+
44
+
45
+ def test_repository_allowlisted_values_empty_for_an_unmapped_repo(
46
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
47
+ ) -> None:
48
+ identity_path = tmp_path / "local-identity.json"
49
+ _write_allowlist_identity(identity_path, "AllowOwner/allow-repo")
50
+ monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
51
+ repository_root = tmp_path / "repo"
52
+ _init_repo_with_github_origin(repository_root, "OtherOwner/other-repo")
53
+ assert repository_allowlisted_values(repository_root) == frozenset()
54
+
55
+
56
+ def test_github_https_origin_yields_lowercased_owner_repo_slug() -> None:
57
+ assert _owner_repo_slug_from_origin_url("https://github.com/Owner/Repo.git") == "owner/repo"
58
+
59
+
60
+ def test_github_ssh_origin_yields_lowercased_owner_repo_slug() -> None:
61
+ assert _owner_repo_slug_from_origin_url("git@github.com:Owner/Repo.git") == "owner/repo"
62
+
63
+
64
+ def test_spoofed_host_origin_yields_no_slug() -> None:
65
+ assert _owner_repo_slug_from_origin_url("https://evil.test/Owner/Repo.git") is None
66
+
67
+
68
+ def _init_repo_with_github_origin(repository_root: Path, origin_slug: str) -> None:
69
+ repository_root.mkdir(parents=True, exist_ok=True)
70
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
71
+ origin_url = "https://github.com/" + origin_slug + ".git"
72
+ subprocess.run(
73
+ ["git", "remote", "add", "origin", origin_url],
74
+ cwd=repository_root,
75
+ check=True,
76
+ )
77
+
78
+
79
+ def test_repository_with_exempt_origin_is_exempt(
80
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
81
+ ) -> None:
82
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
83
+ repository_root = tmp_path / "repo"
84
+ _init_repo_with_github_origin(repository_root, "ExemptOwner/exempt-repo")
85
+ assert _is_repository_exempt_from_pii_scan(repository_root) is True
86
+
87
+
88
+ def test_repository_with_unlisted_origin_is_scanned(
89
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
90
+ ) -> None:
91
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
92
+ repository_root = tmp_path / "repo"
93
+ _init_repo_with_github_origin(repository_root, "OtherOwner/other-repo")
94
+ assert _is_repository_exempt_from_pii_scan(repository_root) is False
95
+
96
+
97
+ def test_repository_without_origin_is_never_exempt(tmp_path: Path) -> None:
98
+ repository_root = tmp_path / "repo"
99
+ repository_root.mkdir()
100
+ subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
101
+ assert _is_repository_exempt_from_pii_scan(repository_root) is False
102
+
103
+
104
+ def test_owner_repo_slug_strips_a_trailing_slash_origin() -> None:
105
+ assert _owner_repo_slug_from_origin_url("https://github.com/Owner/Repo/") == "owner/repo"
@@ -0,0 +1,108 @@
1
+ """Behavioral tests for the commit repository-path resolution helpers.
2
+
3
+ Covers reading the repository a commit command targets, so the staged PII scan
4
+ runs against the named path rather than the session working directory::
5
+
6
+ git -C /a -C sub commit -> /a/sub (multiple -C compose)
7
+ git -C /a/b -C /c/d commit -> /c/d (a later absolute -C resets)
8
+ cd "/my repo" && git commit -> /my repo (quoted cd path, shlex-parsed)
9
+ git commit -> None (runs in the session cwd)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from pii_prevention_blocker_parts.repository_resolution import (
17
+ compose_command_working_directory,
18
+ expand_user_directory,
19
+ refusal_reason_for_unresolved_repository,
20
+ )
21
+
22
+
23
+ def test_single_absolute_dash_c_is_the_target() -> None:
24
+ assert compose_command_working_directory("git -C /repo commit -m x") == "/repo"
25
+
26
+
27
+ def test_relative_dash_c_composes_onto_a_leading_absolute_dash_c() -> None:
28
+ composed = compose_command_working_directory("git -C /a -C sub commit -m x")
29
+ assert composed is not None
30
+ assert Path(composed) == Path("/a") / "sub"
31
+
32
+
33
+ def test_later_absolute_dash_c_resets_the_composition() -> None:
34
+ assert compose_command_working_directory("git -C /a/b -C /c/d commit -m x") == "/c/d"
35
+
36
+
37
+ def test_quoted_dash_c_with_spaces_is_read_whole() -> None:
38
+ assert compose_command_working_directory('git -C "/my repo" commit -m x') == "/my repo"
39
+
40
+
41
+ def test_leading_cd_supplies_the_target_when_no_dash_c() -> None:
42
+ assert compose_command_working_directory("cd /work/repo && git commit -m x") == "/work/repo"
43
+
44
+
45
+ def test_quoted_cd_path_with_spaces_is_read_whole() -> None:
46
+ assert compose_command_working_directory('cd "/my repo" && git commit -m x') == "/my repo"
47
+
48
+
49
+ def test_plain_commit_names_no_target() -> None:
50
+ assert compose_command_working_directory("git commit -m x") is None
51
+
52
+
53
+ def test_expand_user_directory_expands_tilde() -> None:
54
+ expanded = expand_user_directory("~/repo")
55
+ assert expanded is not None
56
+ assert not expanded.startswith("~")
57
+ assert Path(expanded).name == "repo"
58
+
59
+
60
+ def test_expand_user_directory_passes_none_through() -> None:
61
+ assert expand_user_directory(None) is None
62
+
63
+
64
+ def test_refusal_names_the_attempted_path() -> None:
65
+ reason = refusal_reason_for_unresolved_repository("/gone/repo")
66
+ assert "repository root" in reason
67
+ assert "/gone/repo" in reason
68
+
69
+
70
+ def test_refusal_names_the_session_cwd_when_no_path_given() -> None:
71
+ reason = refusal_reason_for_unresolved_repository(None)
72
+ assert "repository root" in reason
73
+ assert "session working directory" in reason
74
+
75
+
76
+ def test_windows_git_exe_dash_c_is_the_target() -> None:
77
+ assert (
78
+ compose_command_working_directory(r'git.exe -C "/srv/repo" commit -m x')
79
+ == "/srv/repo"
80
+ )
81
+
82
+
83
+ def test_leading_cd_with_git_exe_supplies_the_target() -> None:
84
+ assert (
85
+ compose_command_working_directory("cd /work/repo && git.exe commit -m x")
86
+ == "/work/repo"
87
+ )
88
+
89
+
90
+ def test_leading_pushd_with_git_exe_supplies_the_target() -> None:
91
+ assert (
92
+ compose_command_working_directory("pushd /work/repo; git.exe commit -m x")
93
+ == "/work/repo"
94
+ )
95
+
96
+
97
+ def test_quoted_cd_path_with_git_exe_is_read_whole() -> None:
98
+ assert (
99
+ compose_command_working_directory('cd "/my repo" && git.exe commit -m x')
100
+ == "/my repo"
101
+ )
102
+
103
+
104
+ def test_dash_c_wins_over_leading_cd() -> None:
105
+ assert (
106
+ compose_command_working_directory("cd /ignored && git -C /named commit -m x")
107
+ == "/named"
108
+ )
@@ -309,7 +309,7 @@ class AttachmentRecordIterator:
309
309
 
310
310
  def __iter__(self) -> Iterator[tuple[dict[str, object], int, int]]:
311
311
  try:
312
- jsonl_file_handle = io.open(self._jsonl_file_path, "rb")
312
+ jsonl_file_handle = io.open(self._jsonl_file_path, "rb") # noqa: SIM115
313
313
  except (FileNotFoundError, OSError):
314
314
  self.final_line_number = self._start_line_number
315
315
  self.final_byte_offset = self._start_offset
@@ -419,14 +419,13 @@ def save_offsets(
419
419
  state_file_parent = os.path.dirname(state_file_path)
420
420
  if state_file_parent:
421
421
  os.makedirs(state_file_parent, exist_ok=True)
422
- temporary_file_handle = tempfile.NamedTemporaryFile(
422
+ with tempfile.NamedTemporaryFile(
423
423
  mode="w",
424
424
  encoding="utf-8",
425
425
  dir=state_file_parent or None,
426
426
  delete=False,
427
- )
428
- temporary_file_path = temporary_file_handle.name
429
- try:
427
+ ) as temporary_file_handle:
428
+ temporary_file_path = temporary_file_handle.name
430
429
  try:
431
430
  json.dump(
432
431
  offset_by_jsonl_path,
@@ -436,8 +435,14 @@ def save_offsets(
436
435
  )
437
436
  temporary_file_handle.flush()
438
437
  os.fsync(temporary_file_handle.fileno())
439
- finally:
438
+ except Exception:
440
439
  temporary_file_handle.close()
440
+ try:
441
+ os.unlink(temporary_file_path)
442
+ except OSError:
443
+ pass
444
+ raise
445
+ try:
441
446
  os.replace(temporary_file_path, state_file_path)
442
447
  except Exception:
443
448
  try:
@@ -468,15 +473,12 @@ def _acquire_offsets_lock(state_file_path: str) -> Iterator[None]:
468
473
  lock_parent_directory = os.path.dirname(lock_file_path)
469
474
  if lock_parent_directory:
470
475
  os.makedirs(lock_parent_directory, exist_ok=True)
471
- lock_file_handle = io.open(lock_file_path, "a+", encoding="utf-8")
472
- try:
476
+ with io.open(lock_file_path, "a+", encoding="utf-8") as lock_file_handle:
473
477
  _lock_file_handle_blocking(lock_file_handle)
474
478
  try:
475
479
  yield
476
480
  finally:
477
481
  _unlock_file_handle(lock_file_handle)
478
- finally:
479
- lock_file_handle.close()
480
482
 
481
483
 
482
484
  def _lock_file_handle_blocking(lock_file_handle: IO[str]) -> None:
@@ -10,7 +10,6 @@ When you commit in a submodule, this hook:
10
10
  This prevents the "lost work" issue where submodule commits aren't tracked by parent.
11
11
  """
12
12
 
13
- import os
14
13
  import subprocess
15
14
  import sys
16
15
  from pathlib import Path
@@ -21,7 +20,7 @@ def run_git(*args: str, cwd: Path | None = None) -> str:
21
20
  result = subprocess.run(
22
21
  ["git"] + list(args),
23
22
  cwd=cwd,
24
- capture_output=True,
23
+ check=False, capture_output=True,
25
24
  text=True,
26
25
  )
27
26
  return result.stdout.strip()
@@ -74,7 +73,7 @@ def main() -> int:
74
73
 
75
74
  diff_result = subprocess.run(
76
75
  ["git", "diff", "--cached", "--quiet"],
77
- cwd=parent_repo,
76
+ check=False, cwd=parent_repo,
78
77
  )
79
78
 
80
79
  if diff_result.returncode == 0:
@@ -89,7 +88,7 @@ Co-Authored-By: Claude <noreply@anthropic.com>"""
89
88
 
90
89
  subprocess.run(
91
90
  ["git", "commit", "-m", full_commit_msg],
92
- cwd=parent_repo,
91
+ check=False, cwd=parent_repo,
93
92
  )
94
93
 
95
94
  print("Parent updated successfully.")
@@ -39,7 +39,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
39
39
  | `hosted_hook_runner.py` | `run_hook_capturing_output()` — shared runner that runs one dispatcher-hosted hook in-process via runpy and returns its captured stdout and crash flag |
40
40
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
41
41
  | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
42
- | `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults), plus the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`); also composes the ssh enforcer's two deny messages that quote the NAS values |
42
+ | `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`, keyed by owner/repo slug) read from the git-ignored local file whose path `CLAUDE_LOCAL_IDENTITY_PATH` may point elsewhere; also composes the ssh enforcer's two deny messages that quote the NAS values |
43
43
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
44
44
  | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
45
45
  | `nas_ssh_binary_enforcer_constants.py` | Bash tool name, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, and the batch-mode pattern for the NAS ssh binary enforcer |
@@ -87,5 +87,5 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
87
87
  - Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
88
88
  - Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
89
89
  - `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they are shared across many hooks.
90
- - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` to resolve private NAS values (with committed placeholder defaults) and the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`).
90
+ - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` (its path overridable via `CLAUDE_LOCAL_IDENTITY_PATH`) to resolve private NAS values (with committed placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`).
91
91
 
@@ -22,7 +22,6 @@ ALL_BANNED_IDENTIFIERS: frozenset[str] = frozenset(
22
22
  "cnt",
23
23
  "btn",
24
24
  "idx",
25
- "tmp",
26
25
  "msg",
27
26
  "elem",
28
27
  "val",
@@ -10,7 +10,6 @@ module.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
-
14
13
  ALL_CONFIG_DIRECTORY_NAMES = frozenset(
15
14
  {
16
15
  "anthropic_plan_scripts_constants",
@@ -23,6 +22,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
23
22
  "pr_converge_scripts_constants",
24
23
  "bugteam_scripts_constants",
25
24
  "usage_pause_constants",
25
+ "codex_review_scripts_constants",
26
26
  "dev_env_scripts_constants",
27
27
  }
28
28
  )