claude-dev-env 1.93.1 → 1.94.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 (215) hide show
  1. package/_shared/pr-loop/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/audit-contract.md +1 -1
  3. package/_shared/pr-loop/gh-payloads.md +3 -3
  4. package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
  5. package/_shared/pr-loop/scripts/README.md +2 -2
  6. package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
  8. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  9. package/_shared/pr-loop/state-schema.md +3 -22
  10. package/bin/CLAUDE.md +1 -1
  11. package/bin/install.mjs +2 -7
  12. package/bin/install.test.mjs +10 -4
  13. package/hooks/blocking/CLAUDE.md +2 -0
  14. package/hooks/blocking/pii_commit_command.py +398 -0
  15. package/hooks/blocking/pii_payload_scan.py +133 -0
  16. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  17. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  18. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  19. package/hooks/hooks_constants/CLAUDE.md +2 -2
  20. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  21. package/hooks/hooks_constants/local_identity.py +65 -15
  22. package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
  23. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  24. package/hooks/hooks_constants/test_local_identity.py +68 -0
  25. package/package.json +1 -1
  26. package/scripts/CLAUDE.md +10 -1
  27. package/scripts/Get-SessionAccount.ps1 +210 -0
  28. package/scripts/tests/CLAUDE.md +15 -1
  29. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  30. package/skills/CLAUDE.md +3 -25
  31. package/skills/anthropic-plan/SKILL.md +1 -13
  32. package/skills/autoconverge/CLAUDE.md +5 -2
  33. package/skills/autoconverge/SKILL.md +112 -387
  34. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  35. package/skills/autoconverge/reference/closing-report.md +9 -2
  36. package/skills/autoconverge/reference/convergence.md +26 -22
  37. package/skills/autoconverge/reference/headless-safety.md +44 -0
  38. package/skills/autoconverge/reference/multi-pr.md +88 -0
  39. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  40. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  41. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  42. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  43. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  44. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  45. package/skills/autoconverge/workflow/converge.mjs +284 -94
  46. package/skills/bugteam/CLAUDE.md +1 -1
  47. package/skills/bugteam/CONSTRAINTS.md +3 -3
  48. package/skills/bugteam/PROMPTS.md +8 -19
  49. package/skills/bugteam/SKILL.md +13 -14
  50. package/skills/bugteam/reference/CLAUDE.md +1 -1
  51. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  52. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  53. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  54. package/skills/bugteam/reference/team-setup.md +11 -11
  55. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  56. package/skills/bugteam/scripts/README.md +20 -8
  57. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  58. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  59. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  60. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  61. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  62. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  63. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  64. package/skills/copilot-finding-triage/SKILL.md +80 -28
  65. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  66. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  67. package/skills/everything-search/SKILL.md +1 -2
  68. package/skills/grokify/SKILL.md +58 -0
  69. package/skills/grokify/templates/handoff-template.md +92 -0
  70. package/skills/orchestrator/SKILL.md +12 -8
  71. package/skills/orchestrator-refresh/SKILL.md +3 -3
  72. package/skills/pr-converge/SKILL.md +132 -233
  73. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  74. package/skills/pr-converge/reference/examples.md +37 -25
  75. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  76. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  77. package/skills/pr-converge/reference/per-tick.md +64 -41
  78. package/skills/pr-converge/reference/state-schema.md +27 -2
  79. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  80. package/skills/pr-converge/scripts/README.md +4 -3
  81. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  82. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  83. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  84. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  85. package/skills/pr-converge/scripts/conftest.py +8 -0
  86. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  87. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  88. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  89. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  90. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  91. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  92. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  93. package/skills/privacy-hygiene/SKILL.md +4 -3
  94. package/skills/reviewer-gates/SKILL.md +26 -8
  95. package/skills/session-log/SKILL.md +11 -12
  96. package/skills/session-tidy/SKILL.md +40 -30
  97. package/skills/skill-builder/CLAUDE.md +13 -10
  98. package/skills/skill-builder/SKILL.md +31 -31
  99. package/skills/skill-builder/references/CLAUDE.md +10 -6
  100. package/skills/skill-builder/references/delegation-map.md +13 -4
  101. package/skills/skill-builder/references/description-field.md +113 -0
  102. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  103. package/skills/skill-builder/references/skill-modularity.md +113 -0
  104. package/skills/skill-builder/references/skill-types.md +4 -0
  105. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  106. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  107. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  108. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  109. package/skills/skill-builder/workflows/new-skill.md +34 -10
  110. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  111. package/skills/usage-pause/SKILL.md +10 -14
  112. package/skills/bdd-protocol/CLAUDE.md +0 -26
  113. package/skills/bdd-protocol/SKILL.md +0 -30
  114. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  115. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  116. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  117. package/skills/bg-agent/CLAUDE.md +0 -17
  118. package/skills/bg-agent/SKILL.md +0 -69
  119. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  120. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  121. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  122. package/skills/caveman/CLAUDE.md +0 -15
  123. package/skills/caveman/SKILL.md +0 -38
  124. package/skills/code/CLAUDE.md +0 -17
  125. package/skills/code/SKILL.md +0 -178
  126. package/skills/deep-research/CLAUDE.md +0 -17
  127. package/skills/deep-research/SKILL.md +0 -80
  128. package/skills/findbugs/CLAUDE.md +0 -20
  129. package/skills/findbugs/SKILL.md +0 -241
  130. package/skills/fixbugs/CLAUDE.md +0 -19
  131. package/skills/fixbugs/SKILL.md +0 -142
  132. package/skills/gh-paginate/CLAUDE.md +0 -18
  133. package/skills/gh-paginate/SKILL.md +0 -84
  134. package/skills/gotcha/CLAUDE.md +0 -33
  135. package/skills/gotcha/SKILL.md +0 -73
  136. package/skills/implement/CLAUDE.md +0 -27
  137. package/skills/implement/SKILL.md +0 -66
  138. package/skills/implement/scripts/CLAUDE.md +0 -22
  139. package/skills/implement/scripts/append_note.py +0 -133
  140. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  141. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  142. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  143. package/skills/implement/scripts/test_append_note.py +0 -191
  144. package/skills/log-audit/CLAUDE.md +0 -20
  145. package/skills/log-audit/SKILL.md +0 -66
  146. package/skills/log-audit/reference/CLAUDE.md +0 -9
  147. package/skills/log-audit/reference/charter.md +0 -52
  148. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  149. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  150. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  151. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  152. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  153. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  154. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  155. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  156. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  157. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  158. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  159. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  160. package/skills/logifix/CLAUDE.md +0 -36
  161. package/skills/logifix/SKILL.md +0 -69
  162. package/skills/logifix/scripts/CLAUDE.md +0 -16
  163. package/skills/logifix/scripts/logifix.ps1 +0 -205
  164. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  165. package/skills/monitor-open-prs/SKILL.md +0 -88
  166. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  167. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  168. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  170. package/skills/post-audit-findings/SKILL.md +0 -80
  171. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  172. package/skills/pr-consistency-audit/SKILL.md +0 -116
  173. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  174. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  175. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  176. package/skills/pr-fix-protocol/SKILL.md +0 -65
  177. package/skills/pr-review-responder/CLAUDE.md +0 -35
  178. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  179. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  180. package/skills/pr-review-responder/README.md +0 -168
  181. package/skills/pr-review-responder/SKILL.md +0 -203
  182. package/skills/pr-review-responder/TESTING.md +0 -350
  183. package/skills/pr-scope-resolve/SKILL.md +0 -45
  184. package/skills/pre-compact/CLAUDE.md +0 -24
  185. package/skills/pre-compact/SKILL.md +0 -134
  186. package/skills/qbug/CLAUDE.md +0 -40
  187. package/skills/qbug/SKILL.md +0 -387
  188. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  189. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  190. package/skills/refine/CLAUDE.md +0 -44
  191. package/skills/refine/SKILL.md +0 -262
  192. package/skills/refine/templates/CLAUDE.md +0 -17
  193. package/skills/refine/templates/implementation-notes-template.html +0 -56
  194. package/skills/refine/templates/plan-template.md +0 -60
  195. package/skills/research-mode/CLAUDE.md +0 -35
  196. package/skills/research-mode/SKILL.md +0 -53
  197. package/skills/structure-prompt/CLAUDE.md +0 -42
  198. package/skills/structure-prompt/SKILL.md +0 -44
  199. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  200. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  201. package/skills/structure-prompt/reference/block-classification.md +0 -27
  202. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  203. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  204. package/skills/structure-prompt/reference/cleanup.md +0 -33
  205. package/skills/structure-prompt/reference/constraints.md +0 -33
  206. package/skills/structure-prompt/reference/directives.md +0 -37
  207. package/skills/structure-prompt/reference/examples.md +0 -72
  208. package/skills/structure-prompt/reference/instantiation.md +0 -51
  209. package/skills/structure-prompt/reference/output-contract.md +0 -72
  210. package/skills/structure-prompt/reference/per-category.md +0 -23
  211. package/skills/structure-prompt/reference/persona.md +0 -38
  212. package/skills/structure-prompt/reference/research.md +0 -33
  213. package/skills/structure-prompt/reference/structure.md +0 -28
  214. package/skills/verified-build/CLAUDE.md +0 -33
  215. package/skills/verified-build/SKILL.md +0 -33
@@ -28,6 +28,7 @@ import pii_prevention_blocker as blocker_module # noqa: E402
28
28
  from pii_prevention_blocker import ( # noqa: E402
29
29
  evaluate,
30
30
  evaluate_bash_command,
31
+ evaluate_post_body_texts,
31
32
  evaluate_staged_commit,
32
33
  evaluate_write_edit_payload,
33
34
  extract_git_commit_working_directory,
@@ -903,3 +904,301 @@ def test_git_exe_dash_c_commit_scans_named_repo_not_cwd(tmp_path: Path) -> None:
903
904
  assert dash_c_scan is not None
904
905
  assert "email" in dash_c_scan
905
906
  assert SYNTHETIC_REAL_EMAIL not in dash_c_scan
907
+
908
+
909
+ def _add_repository_origin(repository_root: Path, origin_url: str) -> None:
910
+ subprocess.run(
911
+ ["git", "remote", "add", "origin", origin_url],
912
+ cwd=repository_root,
913
+ check=True,
914
+ capture_output=True,
915
+ text=True,
916
+ )
917
+
918
+
919
+ def test_exempt_repository_commit_skips_staged_pii_scan(
920
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
921
+ ) -> None:
922
+ repository_root = tmp_path / "repo"
923
+ _init_repo_with_staged_email(repository_root)
924
+ _add_repository_origin(
925
+ repository_root, "https://github.com/ExemptOwner/exempt-repo.git"
926
+ )
927
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
928
+ deny_reason = evaluate_bash_command(
929
+ "git commit -m test",
930
+ working_directory=str(repository_root),
931
+ )
932
+ assert deny_reason is None
933
+
934
+
935
+ def test_exempt_repository_matches_ssh_origin_url(
936
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
937
+ ) -> None:
938
+ repository_root = tmp_path / "repo"
939
+ _init_repo_with_staged_email(repository_root)
940
+ _add_repository_origin(
941
+ repository_root, "git@github.com:ExemptOwner/exempt-repo.git"
942
+ )
943
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "exemptowner/EXEMPT-REPO")
944
+ deny_reason = evaluate_bash_command(
945
+ "git commit -m test",
946
+ working_directory=str(repository_root),
947
+ )
948
+ assert deny_reason is None
949
+
950
+
951
+ def test_non_exempt_repository_commit_still_scans_staged_pii(
952
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
953
+ ) -> None:
954
+ repository_root = tmp_path / "repo"
955
+ _init_repo_with_staged_email(repository_root)
956
+ _add_repository_origin(
957
+ repository_root, "https://github.com/OtherOwner/other-repo.git"
958
+ )
959
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
960
+ deny_reason = evaluate_bash_command(
961
+ "git commit -m test",
962
+ working_directory=str(repository_root),
963
+ )
964
+ assert deny_reason is not None
965
+ assert "email" in deny_reason
966
+
967
+
968
+ def test_repository_without_origin_is_not_exempt(
969
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
970
+ ) -> None:
971
+ repository_root = tmp_path / "repo"
972
+ _init_repo_with_staged_email(repository_root)
973
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
974
+ deny_reason = evaluate_bash_command(
975
+ "git commit -m test",
976
+ working_directory=str(repository_root),
977
+ )
978
+ assert deny_reason is not None
979
+ assert "email" in deny_reason
980
+
981
+
982
+ def test_owner_repo_slug_from_https_origin_with_git_suffix() -> None:
983
+ assert (
984
+ blocker_module._owner_repo_slug_from_origin_url(
985
+ "https://github.com/Owner/repo.git"
986
+ )
987
+ == "owner/repo"
988
+ )
989
+
990
+
991
+ def test_owner_repo_slug_from_https_origin_without_git_suffix() -> None:
992
+ assert (
993
+ blocker_module._owner_repo_slug_from_origin_url(
994
+ "https://github.com/Owner/repo"
995
+ )
996
+ == "owner/repo"
997
+ )
998
+
999
+
1000
+ def test_owner_repo_slug_from_scp_ssh_origin() -> None:
1001
+ assert (
1002
+ blocker_module._owner_repo_slug_from_origin_url(
1003
+ "git@github.com:Owner/repo.git"
1004
+ )
1005
+ == "owner/repo"
1006
+ )
1007
+
1008
+
1009
+ def test_owner_repo_slug_from_ssh_scheme_origin() -> None:
1010
+ assert (
1011
+ blocker_module._owner_repo_slug_from_origin_url(
1012
+ "ssh://git@github.com/Owner/repo.git"
1013
+ )
1014
+ == "owner/repo"
1015
+ )
1016
+
1017
+
1018
+ def test_owner_repo_slug_from_trailing_slash_git_suffix() -> None:
1019
+ assert (
1020
+ blocker_module._owner_repo_slug_from_origin_url(
1021
+ "https://github.com/Owner/repo.git/"
1022
+ )
1023
+ == "owner/repo"
1024
+ )
1025
+
1026
+
1027
+ def test_owner_repo_slug_rejects_spoofed_evil_host() -> None:
1028
+ assert (
1029
+ blocker_module._owner_repo_slug_from_origin_url(
1030
+ "https://evil.test/ExemptOwner/exempt-repo.git"
1031
+ )
1032
+ is None
1033
+ )
1034
+
1035
+
1036
+ def test_owner_repo_slug_rejects_notgithub_com_host() -> None:
1037
+ assert (
1038
+ blocker_module._owner_repo_slug_from_origin_url(
1039
+ "https://notgithub.com/Owner/repo.git"
1040
+ )
1041
+ is None
1042
+ )
1043
+
1044
+
1045
+ def test_owner_repo_slug_rejects_github_com_suffix_host() -> None:
1046
+ assert (
1047
+ blocker_module._owner_repo_slug_from_origin_url(
1048
+ "https://github.com.evil.test/Owner/repo.git"
1049
+ )
1050
+ is None
1051
+ )
1052
+
1053
+
1054
+ def test_owner_repo_slug_rejects_invalid_https_port_authority() -> None:
1055
+ assert (
1056
+ blocker_module._owner_repo_slug_from_origin_url(
1057
+ "https://github.com:443.evil.test/Owner/repo.git"
1058
+ )
1059
+ is None
1060
+ )
1061
+
1062
+
1063
+ def test_owner_repo_slug_rejects_invalid_ssh_port_authority() -> None:
1064
+ assert (
1065
+ blocker_module._owner_repo_slug_from_origin_url(
1066
+ "ssh://git@github.com:22.evil.test/Owner/repo.git"
1067
+ )
1068
+ is None
1069
+ )
1070
+
1071
+
1072
+ def test_owner_repo_slug_from_https_origin_with_valid_port() -> None:
1073
+ assert (
1074
+ blocker_module._owner_repo_slug_from_origin_url(
1075
+ "https://github.com:443/Owner/repo.git"
1076
+ )
1077
+ == "owner/repo"
1078
+ )
1079
+
1080
+
1081
+ def test_owner_repo_slug_from_ssh_origin_with_valid_port() -> None:
1082
+ assert (
1083
+ blocker_module._owner_repo_slug_from_origin_url(
1084
+ "ssh://git@github.com:22/Owner/repo.git"
1085
+ )
1086
+ == "owner/repo"
1087
+ )
1088
+
1089
+
1090
+ def test_owner_repo_slug_rejects_path_shaped_origin() -> None:
1091
+ assert (
1092
+ blocker_module._owner_repo_slug_from_origin_url(
1093
+ "C:/repos/ExemptOwner/exempt-repo"
1094
+ )
1095
+ is None
1096
+ )
1097
+
1098
+
1099
+ def test_spoofed_origin_host_still_denies_staged_email(
1100
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1101
+ ) -> None:
1102
+ repository_root = tmp_path / "repo"
1103
+ _init_repo_with_staged_email(repository_root)
1104
+ _add_repository_origin(
1105
+ repository_root, "https://evil.test/ExemptOwner/exempt-repo.git"
1106
+ )
1107
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1108
+ deny_reason = evaluate_bash_command(
1109
+ "git commit -m test",
1110
+ working_directory=str(repository_root),
1111
+ )
1112
+ assert deny_reason is not None
1113
+ assert "email" in deny_reason
1114
+
1115
+
1116
+ def test_invalid_port_authority_origin_still_denies_staged_email(
1117
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1118
+ ) -> None:
1119
+ repository_root = tmp_path / "repo"
1120
+ _init_repo_with_staged_email(repository_root)
1121
+ _add_repository_origin(
1122
+ repository_root,
1123
+ "https://github.com:443.evil.test/ExemptOwner/exempt-repo.git",
1124
+ )
1125
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1126
+ deny_reason = evaluate_bash_command(
1127
+ "git commit -m test",
1128
+ working_directory=str(repository_root),
1129
+ )
1130
+ assert deny_reason is not None
1131
+ assert "email" in deny_reason
1132
+
1133
+
1134
+ def test_path_shaped_origin_still_denies_staged_email(
1135
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1136
+ ) -> None:
1137
+ repository_root = tmp_path / "repo"
1138
+ _init_repo_with_staged_email(repository_root)
1139
+ _add_repository_origin(repository_root, "C:/repos/ExemptOwner/exempt-repo")
1140
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1141
+ deny_reason = evaluate_bash_command(
1142
+ "git commit -m test",
1143
+ working_directory=str(repository_root),
1144
+ )
1145
+ assert deny_reason is not None
1146
+ assert "email" in deny_reason
1147
+
1148
+
1149
+ def test_trailing_slash_git_suffix_origin_still_exempts(
1150
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1151
+ ) -> None:
1152
+ repository_root = tmp_path / "repo"
1153
+ _init_repo_with_staged_email(repository_root)
1154
+ _add_repository_origin(
1155
+ repository_root, "https://github.com/ExemptOwner/exempt-repo.git/"
1156
+ )
1157
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1158
+ deny_reason = evaluate_bash_command(
1159
+ "git commit -m test",
1160
+ working_directory=str(repository_root),
1161
+ )
1162
+ assert deny_reason is None
1163
+
1164
+
1165
+ def test_ssh_scheme_origin_still_exempts(
1166
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1167
+ ) -> None:
1168
+ repository_root = tmp_path / "repo"
1169
+ _init_repo_with_staged_email(repository_root)
1170
+ _add_repository_origin(
1171
+ repository_root, "ssh://git@github.com/ExemptOwner/exempt-repo.git"
1172
+ )
1173
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1174
+ deny_reason = evaluate_bash_command(
1175
+ "git commit -m test",
1176
+ working_directory=str(repository_root),
1177
+ )
1178
+ assert deny_reason is None
1179
+
1180
+
1181
+ def test_write_still_scans_when_repository_is_exempt(
1182
+ monkeypatch: pytest.MonkeyPatch,
1183
+ ) -> None:
1184
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1185
+ deny_reason = evaluate_write_edit_payload(
1186
+ "Write",
1187
+ {
1188
+ "file_path": "notes.md",
1189
+ "content": f"Reach me at {SYNTHETIC_REAL_EMAIL}",
1190
+ },
1191
+ )
1192
+ assert deny_reason is not None
1193
+ assert "email" in deny_reason
1194
+
1195
+
1196
+ def test_post_body_still_scans_when_repository_is_exempt(
1197
+ monkeypatch: pytest.MonkeyPatch,
1198
+ ) -> None:
1199
+ monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1200
+ deny_reason = evaluate_post_body_texts(
1201
+ [f"Reach me at {SYNTHETIC_REAL_EMAIL}"]
1202
+ )
1203
+ assert deny_reason is not None
1204
+ assert "email" in deny_reason
@@ -16,8 +16,18 @@ import subprocess
16
16
  import sys
17
17
 
18
18
  _HOOK_DIR = pathlib.Path(__file__).parent
19
+ _HOOKS_DIR = _HOOK_DIR.parent
19
20
  if str(_HOOK_DIR) not in sys.path:
20
21
  sys.path.insert(0, str(_HOOK_DIR))
22
+ if str(_HOOKS_DIR) not in sys.path:
23
+ sys.path.insert(0, str(_HOOKS_DIR))
24
+
25
+ from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
26
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
27
+ ALL_BASH_HOSTED_HOOK_ENTRIES,
28
+ BASH_TOOL_NAME,
29
+ POWERSHELL_TOOL_NAME,
30
+ )
21
31
 
22
32
  guard_spec = importlib.util.spec_from_file_location(
23
33
  "verdict_directory_write_blocker",
@@ -708,18 +718,43 @@ def _pretooluse_commands_for_matcher(matcher_substring: str) -> list[str]:
708
718
  return matching_commands
709
719
 
710
720
 
711
- def test_guard_is_registered_on_bash() -> None:
721
+ _VERDICT_GUARD_SCRIPT_RELATIVE_PATH = "blocking/verdict_directory_write_blocker.py"
722
+ _BASH_DISPATCHER_BASENAME = "bash_pre_tool_use_dispatcher.py"
723
+
724
+
725
+ def _verdict_guard_hosted_entries() -> list:
726
+ return [
727
+ each_entry
728
+ for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
729
+ if each_entry.script_relative_path == _VERDICT_GUARD_SCRIPT_RELATIVE_PATH
730
+ ]
731
+
732
+
733
+ def _assert_bash_dispatcher_registered_for_matcher(matcher_substring: str) -> None:
734
+ matching_commands = _pretooluse_commands_for_matcher(matcher_substring)
712
735
  assert any(
713
- "verdict_directory_write_blocker.py" in each_command
714
- for each_command in _pretooluse_commands_for_matcher("Bash")
736
+ _BASH_DISPATCHER_BASENAME in each_command
737
+ for each_command in matching_commands
738
+ ), (
739
+ f"bash_pre_tool_use_dispatcher.py must be registered on matcher containing "
740
+ f"{matcher_substring!r}; commands={matching_commands!r}"
715
741
  )
716
742
 
717
743
 
744
+ def test_guard_is_registered_on_bash() -> None:
745
+ _assert_bash_dispatcher_registered_for_matcher("Bash")
746
+ matching_entries = _verdict_guard_hosted_entries()
747
+ assert len(matching_entries) == 1
748
+ assert matching_entries[0].applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
749
+ assert BASH_TOOL_NAME in matching_entries[0].applicable_tool_names
750
+
751
+
718
752
  def test_guard_is_registered_on_powershell() -> None:
719
- assert any(
720
- "verdict_directory_write_blocker.py" in each_command
721
- for each_command in _pretooluse_commands_for_matcher("PowerShell")
722
- )
753
+ _assert_bash_dispatcher_registered_for_matcher("PowerShell")
754
+ matching_entries = _verdict_guard_hosted_entries()
755
+ assert len(matching_entries) == 1
756
+ assert matching_entries[0].applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
757
+ assert POWERSHELL_TOOL_NAME in matching_entries[0].applicable_tool_names
723
758
 
724
759
 
725
760
  def test_hook_subprocess_imports_real_config_when_parent_holds_shadowing_config(
@@ -38,7 +38,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
38
38
  | `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 |
39
39
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
40
40
  | `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
41
- | `local_identity.py` | Loader for the NAS host, ssh user, and ssh port the ssh enforcer guards, resolved from the `CLAUDE_NAS_*` env vars or `~/.claude/local-identity.json` with placeholder defaults; also composes the enforcer's two deny messages that quote those values |
41
+ | `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
42
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
43
43
  | `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 |
44
44
  | `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 |
@@ -85,5 +85,5 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
85
85
  - Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
86
86
  - Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
87
87
  - `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.
88
- - `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` to resolve the private NAS values, keeping a committed placeholder default.
88
+ - `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`).
89
89
 
@@ -22,8 +22,6 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
22
22
  "pr_converge_skill_constants",
23
23
  "pr_converge_scripts_constants",
24
24
  "bugteam_scripts_constants",
25
- "implement_scripts_constants",
26
- "log_audit_constants",
27
25
  "usage_pause_constants",
28
26
  "dev_env_scripts_constants",
29
27
  }
@@ -1,19 +1,23 @@
1
- """Resolve the NAS host, ssh user, and ssh port for the NAS ssh enforcer hook.
1
+ """Resolve local identity values for NAS ssh and PII exempt-repo hooks.
2
2
 
3
- The enforcer ships into ``~/.claude/`` and cannot read a repository file, so it
4
- reads the real NAS values from the environment or a git-ignored file in the
5
- Claude home directory, and it composes the two deny messages that quote those
6
- values. The committed defaults are placeholders.
3
+ The enforcer hooks ship into ``~/.claude/`` and cannot read a repository file,
4
+ so they read real machine values from the environment or a git-ignored file in
5
+ the Claude home directory. This module supplies NAS host/user/port, the two
6
+ NAS deny messages that quote those values, and the owner/repo slug set used by
7
+ ``pii_exempt_repository_slugs`` (``CLAUDE_PII_EXEMPT_REPOS`` /
8
+ ``pii_exempt_repositories``). The committed NAS defaults are placeholders.
7
9
 
8
10
  ::
9
11
 
10
- CLAUDE_NAS_HOST set to a host -> nas_host() returns that host
11
- CLAUDE_NAS_SSH_PORT set to 2200 -> nas_ssh_port() returns 2200
12
- (env unset, no file) -> nas_host() == "nas.example.local"
13
- nas_ssh_port() == 22
12
+ CLAUDE_NAS_HOST set to a host -> nas_host() returns that host
13
+ CLAUDE_NAS_SSH_PORT set to 2200 -> nas_ssh_port() returns 2200
14
+ CLAUDE_PII_EXEMPT_REPOS="Owner/repo" -> {"owner/repo"}
15
+ (env unset, no file) -> nas_host() == "nas.example.local"
16
+ nas_ssh_port() == 22
17
+ pii_exempt_repository_slugs() == frozenset()
14
18
 
15
- Each value comes from its ``CLAUDE_NAS_*`` environment variable, then
16
- ``~/.claude/local-identity.json``, then the placeholder default.
19
+ Each value comes from its environment variable, then
20
+ ``~/.claude/local-identity.json``, then the placeholder default (NAS only).
17
21
  """
18
22
 
19
23
  from __future__ import annotations
@@ -23,6 +27,9 @@ import os
23
27
  from pathlib import Path
24
28
 
25
29
  NAS_HOST_ENV_VAR = "CLAUDE_NAS_HOST"
30
+ PII_EXEMPT_REPOS_ENV_VAR = "CLAUDE_PII_EXEMPT_REPOS"
31
+ PII_EXEMPT_REPOS_JSON_KEY = "pii_exempt_repositories"
32
+ PII_EXEMPT_REPOS_SEPARATOR = ","
26
33
  NAS_SSH_USER_ENV_VAR = "CLAUDE_NAS_SSH_USER"
27
34
  NAS_SSH_PORT_ENV_VAR = "CLAUDE_NAS_SSH_PORT"
28
35
  NAS_JSON_KEY = "nas"
@@ -42,7 +49,7 @@ def _local_identity_file_path() -> Path:
42
49
  return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / LOCAL_IDENTITY_FILE_NAME
43
50
 
44
51
 
45
- def _nas_section_from_local_file() -> dict[str, object]:
52
+ def _identity_dictionary_from_local_file() -> dict[str, object]:
46
53
  identity_path = _local_identity_file_path()
47
54
  if not identity_path.is_file():
48
55
  return {}
@@ -50,12 +57,55 @@ def _nas_section_from_local_file() -> dict[str, object]:
50
57
  parsed_identity = json.loads(identity_path.read_text(encoding="utf-8"))
51
58
  except (OSError, ValueError, UnicodeDecodeError):
52
59
  return {}
53
- if not isinstance(parsed_identity, dict):
54
- return {}
55
- nas_section = parsed_identity.get(NAS_JSON_KEY, {})
60
+ return parsed_identity if isinstance(parsed_identity, dict) else {}
61
+
62
+
63
+ def _nas_section_from_local_file() -> dict[str, object]:
64
+ nas_section = _identity_dictionary_from_local_file().get(NAS_JSON_KEY, {})
56
65
  return nas_section if isinstance(nas_section, dict) else {}
57
66
 
58
67
 
68
+ def _normalized_slug_set(all_raw_slugs: list[object]) -> frozenset[str]:
69
+ return frozenset(
70
+ each_slug.strip().lower()
71
+ for each_slug in all_raw_slugs
72
+ if isinstance(each_slug, str) and each_slug.strip()
73
+ )
74
+
75
+
76
+ def pii_exempt_repository_slugs() -> frozenset[str]:
77
+ """Return owner/repo slugs whose commits skip the staged PII scan.
78
+
79
+ ::
80
+
81
+ CLAUDE_PII_EXEMPT_REPOS="Owner/repo-a, Other/b" -> {"owner/repo-a", "other/b"}
82
+ CLAUDE_PII_EXEMPT_REPOS=" , , " + file lists A -> {"a"} (whitespace falls through)
83
+ (env unset, file lists ["Owner/repo-a"]) -> {"owner/repo-a"}
84
+ (env unset, no file) -> frozenset()
85
+
86
+ A non-empty environment variable (comma-separated) wins. When the variable
87
+ is unset or normalizes to no slugs, the ``pii_exempt_repositories`` list in
88
+ the git-ignored ``~/.claude/local-identity.json`` is used. Slugs are
89
+ lowercased so matching is case-insensitive.
90
+
91
+ Returns:
92
+ Lowercased ``owner/repo`` slugs exempt from staged-commit PII scanning.
93
+ """
94
+ slugs_from_environment = os.environ.get(PII_EXEMPT_REPOS_ENV_VAR)
95
+ if slugs_from_environment is not None:
96
+ normalized_environment_slugs = _normalized_slug_set(
97
+ list(slugs_from_environment.split(PII_EXEMPT_REPOS_SEPARATOR))
98
+ )
99
+ if normalized_environment_slugs:
100
+ return normalized_environment_slugs
101
+ stored_slugs = _identity_dictionary_from_local_file().get(
102
+ PII_EXEMPT_REPOS_JSON_KEY
103
+ )
104
+ if not isinstance(stored_slugs, list):
105
+ return frozenset()
106
+ return _normalized_slug_set(stored_slugs)
107
+
108
+
59
109
  def nas_host() -> str:
60
110
  """Return the NAS host the ssh enforcer guards.
61
111
 
@@ -187,6 +187,22 @@ ALL_STAGED_FILES_COMMAND: tuple[str, ...] = (
187
187
  ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX: tuple[str, ...] = ("git", "show")
188
188
  STAGED_BLOB_PREFIX: str = ":"
189
189
 
190
+ ALL_GIT_ORIGIN_URL_COMMAND: tuple[str, ...] = (
191
+ "git",
192
+ "config",
193
+ "--get",
194
+ "remote.origin.url",
195
+ )
196
+ GIT_URL_SUFFIX: str = ".git"
197
+ GITHUB_COM_HOST: str = "github.com"
198
+ ALL_NETWORK_GIT_URL_SCHEMES: frozenset[str] = frozenset({"http", "https", "ssh"})
199
+ SCP_STYLE_PATH_SEPARATOR: str = ":"
200
+ USERINFO_HOST_SEPARATOR: str = "@"
201
+ URL_SCHEME_SEPARATOR: str = "://"
202
+ WINDOWS_PATH_SEPARATOR: str = "\\"
203
+ POSIX_PATH_SEPARATOR: str = "/"
204
+ MINIMUM_OWNER_REPO_SEGMENT_COUNT: int = 2
205
+
190
206
  BODY_FILE_ENCODING: str = "utf-8"
191
207
  NULL_BYTE_MARKER: bytes = b"\x00"
192
208
  MESSAGE_LINE_SEPARATOR: str = "\n"
@@ -121,6 +121,7 @@ DEFAULT_READABILITY_THRESHOLDS: ReadabilityThresholds = ReadabilityThresholds(
121
121
  __all__ = [
122
122
  "ALL_HEAVY_OPENING_HEADERS",
123
123
  "ALL_HEAVY_TESTING_HEADERS",
124
+ "ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
124
125
  "ALL_READABILITY_CLI_FLAG_TOKENS",
125
126
  "ATOMIC_WRITE_TEMP_SUFFIX",
126
127
  "BLOCKQUOTE_LINE_PATTERN",
@@ -135,7 +136,6 @@ __all__ = [
135
136
  "FLESCH_SYLLABLES_PER_WORD_COEFFICIENT",
136
137
  "FLESCH_WORDS_PER_SENTENCE_COEFFICIENT",
137
138
  "GH_PR_COMMAND_MIN_TOKEN_COUNT",
138
- "ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
139
139
  "HEADING_LINE_PATTERN",
140
140
  "HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION",
141
141
  "HEAVY_SHAPE",
@@ -66,6 +66,74 @@ class TestNasValuesPlaceholderDefault:
66
66
  assert local_identity.nas_ssh_port() == 22
67
67
 
68
68
 
69
+ class TestPiiExemptRepositorySlugs:
70
+ def should_read_slugs_from_the_environment_lowercased(self) -> None:
71
+ with patch.dict(
72
+ os.environ,
73
+ {"CLAUDE_PII_EXEMPT_REPOS": "ExemptOwner/exempt-repo, Other/Repo"},
74
+ clear=False,
75
+ ):
76
+ slugs = local_identity.pii_exempt_repository_slugs()
77
+ assert slugs == frozenset({"exemptowner/exempt-repo", "other/repo"})
78
+
79
+ def should_read_slugs_from_the_local_identity_file(self, tmp_path: Path) -> None:
80
+ claude_home = tmp_path / ".claude"
81
+ claude_home.mkdir()
82
+ (claude_home / "local-identity.json").write_text(
83
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
84
+ encoding="utf-8",
85
+ )
86
+ with patch.dict(os.environ, {}, clear=False):
87
+ os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
88
+ with patch.object(Path, "home", return_value=tmp_path):
89
+ slugs = local_identity.pii_exempt_repository_slugs()
90
+ assert slugs == frozenset({"fileowner/file-repo"})
91
+
92
+ def should_return_an_empty_set_when_no_env_and_no_file(
93
+ self, tmp_path: Path
94
+ ) -> None:
95
+ with patch.dict(os.environ, {}, clear=False):
96
+ os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
97
+ with patch.object(Path, "home", return_value=tmp_path):
98
+ assert local_identity.pii_exempt_repository_slugs() == frozenset()
99
+
100
+ def should_prefer_environment_slugs_over_local_identity_file(
101
+ self, tmp_path: Path
102
+ ) -> None:
103
+ claude_home = tmp_path / ".claude"
104
+ claude_home.mkdir()
105
+ (claude_home / "local-identity.json").write_text(
106
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
107
+ encoding="utf-8",
108
+ )
109
+ with patch.dict(
110
+ os.environ,
111
+ {"CLAUDE_PII_EXEMPT_REPOS": "EnvOwner/env-repo"},
112
+ clear=False,
113
+ ):
114
+ with patch.object(Path, "home", return_value=tmp_path):
115
+ slugs = local_identity.pii_exempt_repository_slugs()
116
+ assert slugs == frozenset({"envowner/env-repo"})
117
+
118
+ def should_fall_through_to_json_when_environment_is_whitespace_only(
119
+ self, tmp_path: Path
120
+ ) -> None:
121
+ claude_home = tmp_path / ".claude"
122
+ claude_home.mkdir()
123
+ (claude_home / "local-identity.json").write_text(
124
+ json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
125
+ encoding="utf-8",
126
+ )
127
+ with patch.dict(
128
+ os.environ,
129
+ {"CLAUDE_PII_EXEMPT_REPOS": " , , "},
130
+ clear=False,
131
+ ):
132
+ with patch.object(Path, "home", return_value=tmp_path):
133
+ slugs = local_identity.pii_exempt_repository_slugs()
134
+ assert slugs == frozenset({"fileowner/file-repo"})
135
+
136
+
69
137
  class TestDenyMessagesQuoteTheResolvedHost:
70
138
  def should_include_the_resolved_host_and_port_in_the_bare_binary_message(
71
139
  self,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.93.1",
3
+ "version": "1.94.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/CLAUDE.md CHANGED
@@ -21,6 +21,7 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
21
21
  | `Install-SweepEmptyDirs.ps1` | Registers `sweep_empty_dirs.py` as a scheduled task on Windows |
22
22
  | `check.ps1` | Runs the full code-quality check suite |
23
23
  | `Show-Asset.ps1` | Opens files on screen, sizing each image window to the image's pixel dimensions (scaled to fit the screen); non-image files open in their default application |
24
+ | `Get-SessionAccount.ps1` | Reports which Claude account the current session is actually logged into by comparing `~/.claude.json`'s CLI login against a `CLAUDE_USER_DATA_DIR` desktop profile's `lastKnownAccountUuid`, recovering the desktop account's email from profile storage when the two accounts differ |
24
25
 
25
26
  ## Subdirectories
26
27
 
@@ -28,10 +29,18 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
28
29
  |---|---|
29
30
  | `dev_env_scripts_constants/` | Named constants (`timing.py`) for scripts in this directory |
30
31
  | `sync_to_cursor/` | Package that builds Cursor `.mdc` files from Claude rules and docs |
31
- | `tests/` | pytest suite for the Python scripts in this directory |
32
+ | `tests/` | pytest suite for the Python scripts and Pester (`*.Tests.ps1`) suite for the PowerShell scripts in this directory |
32
33
 
33
34
  ## Running tests
34
35
 
36
+ Python scripts (pytest):
37
+
35
38
  ```bash
36
39
  python -m pytest packages/claude-dev-env/scripts/tests/
37
40
  ```
41
+
42
+ PowerShell scripts (Pester 5+, `*.Tests.ps1`):
43
+
44
+ ```powershell
45
+ Invoke-Pester -Path packages/claude-dev-env/scripts/tests/
46
+ ```