claude-dev-env 4.2.0 → 4.3.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 (78) hide show
  1. package/.agents/skills/_shared/pr-loop/scripts/durable_post_lint.py +242 -0
  2. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py +106 -0
  3. package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint.py +332 -0
  4. package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py +25 -0
  5. package/.agents/skills/issue-tracker/SKILL.md +6 -0
  6. package/.agents/skills/pull-request/SKILL.md +218 -0
  7. package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
  8. package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
  9. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
  10. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +61 -0
  11. package/.agents/skills/pull-request/scripts/pull_request.py +276 -0
  12. package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
  13. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +49 -0
  14. package/.agents/skills/pull-request/scripts/test_pull_request.py +398 -0
  15. package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
  16. package/.agents/skills/test-runner/SKILL.md +69 -0
  17. package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
  18. package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
  19. package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
  20. package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
  21. package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
  22. package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
  23. package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
  24. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  25. package/bin/ever-shipped-skills.mjs +2 -0
  26. package/bin/ever-shipped-skills.test.mjs +8 -0
  27. package/bin/install.mjs +9 -1
  28. package/bin/install.prune.test.mjs +90 -0
  29. package/bin/install.test.mjs +24 -7
  30. package/hooks/blocking/pii_prevention_blocker.py +32 -51
  31. package/hooks/blocking/test_bash_post_call_dispatcher.py +5 -28
  32. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
  33. package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
  34. package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
  35. package/hooks/gh_post_body_texts.py +195 -0
  36. package/hooks/hooks.json +0 -15
  37. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -1
  38. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
  39. package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
  40. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +0 -7
  41. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
  42. package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
  43. package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
  44. package/hooks/observability/test_failure_recorder.py +1 -1
  45. package/hooks/pyproject.toml +1 -1
  46. package/hooks/test_gh_post_body_texts.py +79 -0
  47. package/package.json +1 -1
  48. package/rules/durable-post-artifacts.md +31 -20
  49. package/rules/gh-cli-conventions.md +23 -13
  50. package/rules/git-workflow.md +21 -8
  51. package/scripts/policy_lint/adapter_configuration.py +53 -12
  52. package/scripts/repository_checks/config/constants.py +0 -5
  53. package/scripts/tests/test_adapter_configuration.py +54 -0
  54. package/hooks/_gh_pr_author_swap_utils.py +0 -1225
  55. package/hooks/blocking/conventional_pr_title_gate.py +0 -443
  56. package/hooks/blocking/gh_body_arg_blocker.py +0 -176
  57. package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
  58. package/hooks/blocking/gh_pr_author_restore.py +0 -105
  59. package/hooks/blocking/pr_description_writer_gate.py +0 -256
  60. package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
  61. package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
  62. package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
  63. package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
  64. package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
  65. package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
  66. package/hooks/blocking/test_preflight_check.py +0 -343
  67. package/hooks/blocking/test_test_preflight_check.py +0 -80
  68. package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
  69. package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
  70. package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
  71. package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
  72. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
  73. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
  74. package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
  75. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
  76. package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
  77. package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
  78. package/hooks/test__gh_pr_author_swap_utils.py +0 -333
@@ -1,23 +1,18 @@
1
1
  #!/usr/bin/env python3
2
2
  """PreToolUse hook: block writes, durable posts, and commits that carry PII.
3
3
 
4
- Surfaces guarded:
5
-
6
- - Write / Edit / MultiEdit — new content about to land on disk
7
- - Bash / PowerShell ``gh`` post subcommands and GitHub MCP post tools — durable
8
- bodies
9
- - Bash / PowerShell ``git commit`` (including ``git.exe`` and flag forms) —
10
- staged file contents about to become history. Commit message bodies are out
11
- of scope; only staged blob text is scanned.
12
-
13
- Payload and post-body scanning live in ``pii_payload_scan``; git-commit command
14
- detection lives in ``pii_commit_command``; both re-export through this entry
15
- module so the hooks.json wiring and every importer resolve unchanged.
16
-
17
- A repository named in ``CLAUDE_PII_EXEMPT_REPOS`` or the ``pii_exempt_repositories``
18
- list in ``~/.claude/local-identity.json`` skips the staged-commit scan; a
19
- repository without a readable origin remote is never exempt. Exact values listed
20
- under ``pii_allowlisted_values`` for a repository's origin slug may pass the
4
+ ::
5
+
6
+ Write/Edit/MultiEdit -> scan new disk content
7
+ gh post / GitHub MCP -> scan durable bodies
8
+ git commit -> scan staged blob text, not the commit message
9
+
10
+ ``pii_payload_scan`` and ``pii_commit_command`` re-export through this
11
+ entry module so hooks.json wiring and importers stay unchanged.
12
+ ``CLAUDE_PII_EXEMPT_REPOS`` or ``pii_exempt_repositories`` in
13
+ ``~/.claude/local-identity.json`` skip the staged-commit scan. A repo
14
+ without a readable origin remote is never exempt.
15
+ ``pii_allowlisted_values`` for a repository origin slug may pass
21
16
  Write/Edit and staged-commit scans in that repository only.
22
17
  """
23
18
 
@@ -56,7 +51,7 @@ try:
56
51
  )
57
52
  from pii_scanner import is_path_exempt_from_pii_scan, scan_text_for_pii
58
53
  from precommit_code_rules_gate import resolve_repository_root
59
- from volatile_path_in_post_blocker import (
54
+ from gh_post_body_texts import (
60
55
  extract_gh_post_body_texts_for_privacy_gate,
61
56
  extract_mcp_body_texts,
62
57
  )
@@ -124,7 +119,8 @@ def list_staged_file_paths(
124
119
  try:
125
120
  completed_process = subprocess.run(
126
121
  list(ALL_STAGED_FILES_COMMAND),
127
- check=False, capture_output=True,
122
+ check=False,
123
+ capture_output=True,
128
124
  text=True,
129
125
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
130
126
  cwd=str(repository_root),
@@ -148,7 +144,8 @@ def _git_show_staged_blob(
148
144
  try:
149
145
  completed_process = subprocess.run(
150
146
  list(ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX) + [staged_blob_reference],
151
- check=False, capture_output=True,
147
+ check=False,
148
+ capture_output=True,
152
149
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
153
150
  cwd=str(repository_root),
154
151
  )
@@ -197,9 +194,7 @@ def _scan_staged_path(
197
194
  repository_root: Path, relative_path: str, all_allowlisted_values: frozenset[str]
198
195
  ) -> str | None:
199
196
  """Return a deny reason for one staged path, or None when it is clean."""
200
- staged_text, unscannable_reason = read_staged_file_text(
201
- repository_root, relative_path
202
- )
197
+ staged_text, unscannable_reason = read_staged_file_text(repository_root, relative_path)
203
198
  if unscannable_reason is not None:
204
199
  return unscannable_reason
205
200
  if staged_text is None:
@@ -217,9 +212,9 @@ def _scan_staged_path(
217
212
  def evaluate_staged_commit(repository_root: Path) -> str | None:
218
213
  """Return a deny reason when staged content carries PII or is unscannable.
219
214
 
220
- Fail-closed: git list/show failures and unscannable blobs deny the commit
221
- rather than treating unread content as clean. A value in the repository's
222
- PII allowlist is dropped from the findings, so its commits may carry it.
215
+ Git list or show failures and unscannable blobs deny the commit. A value
216
+ in the repository's PII allowlist is dropped from the findings, so its
217
+ commits may carry it.
223
218
 
224
219
  Args:
225
220
  repository_root: Repository whose index is about to be committed.
@@ -234,22 +229,18 @@ def evaluate_staged_commit(repository_root: Path) -> str | None:
234
229
  for each_relative_path in all_relative_paths:
235
230
  if is_path_exempt_from_pii_scan(each_relative_path):
236
231
  continue
237
- deny_reason = _scan_staged_path(
238
- repository_root, each_relative_path, all_allowlisted_values
239
- )
232
+ deny_reason = _scan_staged_path(repository_root, each_relative_path, all_allowlisted_values)
240
233
  if deny_reason is not None:
241
234
  return deny_reason
242
235
  return None
243
236
 
244
237
 
245
- def _evaluate_commit_staged_content(
246
- bash_command: str, working_directory: str | None
247
- ) -> str | None:
238
+ def _evaluate_commit_staged_content(bash_command: str, working_directory: str | None) -> str | None:
248
239
  """Scan the staged content of the repository the commit command targets.
249
240
 
250
- Resolves the repository from the command's ``-C``/``cd`` path, falling back
251
- to the session working directory only when the command names none, so a
252
- removed cwd never blocks a commit that targets a valid repo.
241
+ Resolves the repository from the command's ``-C`` or ``cd`` path. Falls
242
+ back to the session working directory only when the command names none. A
243
+ removed cwd does not block a commit that targets a valid repo.
253
244
 
254
245
  Args:
255
246
  bash_command: The Bash or PowerShell tool command string.
@@ -259,9 +250,7 @@ def _evaluate_commit_staged_content(
259
250
  Deny reason text, or None when the staged content is clean or exempt.
260
251
  """
261
252
  command_directory = compose_command_working_directory(bash_command)
262
- attempted_directory = (
263
- command_directory if command_directory is not None else working_directory
264
- )
253
+ attempted_directory = command_directory if command_directory is not None else working_directory
265
254
  expanded_directory = expand_user_directory(attempted_directory)
266
255
  repository_root = resolve_repository_root(expanded_directory)
267
256
  if repository_root is None:
@@ -271,9 +260,7 @@ def _evaluate_commit_staged_content(
271
260
  return evaluate_staged_commit(repository_root)
272
261
 
273
262
 
274
- def evaluate_bash_command(
275
- bash_command: str, working_directory: str | None
276
- ) -> str | None:
263
+ def evaluate_bash_command(bash_command: str, working_directory: str | None) -> str | None:
277
264
  """Return a deny reason for a shell gh post or git commit with PII.
278
265
 
279
266
  Args:
@@ -284,10 +271,8 @@ def evaluate_bash_command(
284
271
  Returns:
285
272
  Deny reason text, or None when the command is clean or out of scope.
286
273
  """
287
- all_post_bodies, body_file_failure_reason = (
288
- extract_gh_post_body_texts_for_privacy_gate(
289
- bash_command, working_directory=working_directory
290
- )
274
+ all_post_bodies, body_file_failure_reason = extract_gh_post_body_texts_for_privacy_gate(
275
+ bash_command, working_directory=working_directory
291
276
  )
292
277
  if body_file_failure_reason is not None:
293
278
  return body_file_failure_reason
@@ -308,9 +293,7 @@ def _evaluate_shell_tool(
308
293
  return None
309
294
  working_directory_value = all_tool_input.get("working_directory")
310
295
  working_directory = (
311
- working_directory_value
312
- if isinstance(working_directory_value, str)
313
- else None
296
+ working_directory_value if isinstance(working_directory_value, str) else None
314
297
  )
315
298
  if working_directory is None:
316
299
  cwd_value = payload_by_key.get("cwd")
@@ -332,9 +315,7 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
332
315
  raw_tool_input = payload_by_key.get("tool_input", {})
333
316
  all_tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
334
317
  if tool_name in ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES:
335
- return evaluate_write_edit_payload(
336
- tool_name, all_tool_input, hook_payload=payload_by_key
337
- )
318
+ return evaluate_write_edit_payload(tool_name, all_tool_input, hook_payload=payload_by_key)
338
319
  if tool_name == APPLY_PATCH_TOOL_NAME:
339
320
  return evaluate_apply_patch_payload(all_tool_input, hook_payload=payload_by_key)
340
321
  if tool_name in ALL_SHELL_TOOL_NAMES:
@@ -1,7 +1,7 @@
1
1
  """Behavior tests for the Bash PostToolUse dispatcher.
2
2
 
3
3
  One interpreter start runs every hosted PostToolUse hook a Bash call fires.
4
- The unit tests pin selection, call order, and crash isolation with a fake
4
+ The unit tests pin selection and call order with a fake
5
5
  runner; the end-to-end test drives the real hosted hooks through a real
6
6
  payload and confirms the recorder's own real side effect lands.
7
7
  ``hooks/conftest.py`` already puts the hooks and blocking directories on
@@ -21,13 +21,10 @@ from hooks_constants.hosted_hook_runner import HostedHookRun
21
21
  from tdd_enforcer_parts import content_hash_store
22
22
 
23
23
 
24
- def test_select_applicable_entries_returns_both_hosted_hooks_for_bash() -> None:
24
+ def test_select_applicable_entries_returns_the_hosted_hook_for_bash() -> None:
25
25
  all_entries = select_applicable_entries(BASH_TOOL_NAME)
26
26
  all_script_paths = [each_entry.script_relative_path for each_entry in all_entries]
27
- assert all_script_paths == [
28
- "blocking/gh_pr_author_restore.py",
29
- "observability/test_failure_recorder.py",
30
- ]
27
+ assert all_script_paths == ["observability/test_failure_recorder.py"]
31
28
 
32
29
 
33
30
  def test_select_applicable_entries_returns_none_for_a_write_tool() -> None:
@@ -48,27 +45,7 @@ def test_dispatch_runs_every_hosted_hook_in_roster_order(
48
45
 
49
46
  dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
50
47
 
51
- assert all_call_paths == ["gh_pr_author_restore.py", "test_failure_recorder.py"]
52
-
53
-
54
- def test_dispatch_runs_the_later_hook_after_an_earlier_one_crashes(
55
- monkeypatch: pytest.MonkeyPatch,
56
- ) -> None:
57
- all_call_paths: list[str] = []
58
-
59
- def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
60
- del payload_text
61
- script_name = Path(script_path).name
62
- all_call_paths.append(script_name)
63
- if script_name == "gh_pr_author_restore.py":
64
- return HostedHookRun(captured_stdout="", did_crash=True)
65
- return HostedHookRun(captured_stdout="", did_crash=False)
66
-
67
- monkeypatch.setattr("bash_post_call_dispatcher.run_hook_capturing_output", _fake_run_hook)
68
-
69
- dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
70
-
71
- assert all_call_paths == ["gh_pr_author_restore.py", "test_failure_recorder.py"]
48
+ assert all_call_paths == ["test_failure_recorder.py"]
72
49
 
73
50
 
74
51
  def test_main_exits_zero_and_writes_nothing_to_stdout(
@@ -85,7 +62,7 @@ def test_main_exits_zero_and_writes_nothing_to_stdout(
85
62
 
86
63
 
87
64
  def test_end_to_end_real_hosted_hooks_record_a_real_pytest_failure(tmp_path: Path) -> None:
88
- """Drives the real, unfaked roster: both scripts run in one process."""
65
+ """Drive the real recorder through the dispatcher."""
89
66
  test_file = tmp_path / "test_orders.py"
90
67
  test_file.write_text("def test_fulfill(): assert False\n")
91
68
  session_id = "post-dispatcher-session"
@@ -222,7 +222,6 @@ def test_powershell_selects_the_shared_hooks_in_registration_order() -> None:
222
222
  "blocking/cursor_cli_python_misfire_blocker.py",
223
223
  "blocking/unscoped_search_blocker.py",
224
224
  "blocking/pii_prevention_blocker.py",
225
- "blocking/pr_description_writer_gate.py",
226
225
  ]
227
226
 
228
227
 
@@ -270,9 +269,7 @@ def test_dispatcher_denies_subshell_disguised_as_arithmetic_expansion() -> None:
270
269
  the live Bash tool path.
271
270
  """
272
271
  payload_text = _bash_payload("echo $((cd /tmp) && pwd)")
273
- decision, _reason = _decision_from_stdout(
274
- _run_process(_DISPATCHER_SCRIPT, payload_text).stdout
275
- )
272
+ decision, _reason = _decision_from_stdout(_run_process(_DISPATCHER_SCRIPT, payload_text).stdout)
276
273
  assert decision == "deny"
277
274
 
278
275
 
@@ -11,7 +11,7 @@ from code_rules_command_dispatch import ( # noqa: E402
11
11
  check_unanchored_command_dispatch,
12
12
  )
13
13
 
14
- GATE_PATH = "packages/claude-dev-env/hooks/blocking/conventional_pr_title_gate.py"
14
+ GATE_PATH = "packages/claude-dev-env/hooks/blocking/block_main_commit.py"
15
15
 
16
16
  UNANCHORED_SOURCE = (
17
17
  "import re\n"
@@ -84,7 +84,7 @@ def test_ignores_file_outside_hooks_blocking() -> None:
84
84
 
85
85
 
86
86
  def test_ignores_test_file() -> None:
87
- test_path = "packages/claude-dev-env/hooks/blocking/test_conventional_pr_title_gate.py"
87
+ test_path = "packages/claude-dev-env/hooks/blocking/test_block_main_commit.py"
88
88
  assert check_unanchored_command_dispatch(UNANCHORED_SOURCE, test_path) == []
89
89
 
90
90
 
@@ -42,6 +42,14 @@ SYNTHETIC_REAL_EMAIL = "owner.fixture@acme-corp.example.io"
42
42
  SYNTHETIC_HOME_PATH = r"C:\Users\fixture_commit_user\secret.txt"
43
43
  SYNTHETIC_PRIVATE_IP = "10.44.12.9"
44
44
  SYNTHETIC_SAFE_BODY = "All checks pass. Contact user@example.com for docs."
45
+ HASH_MARKER = "#"
46
+
47
+
48
+ def test_post_body_extraction_has_a_neutral_module_owner() -> None:
49
+ assert (
50
+ blocker_module.extract_gh_post_body_texts_for_privacy_gate.__module__
51
+ == "gh_post_body_texts"
52
+ )
45
53
 
46
54
 
47
55
  def _run_hook(payload: dict[str, object]) -> tuple[int, str]:
@@ -370,10 +378,7 @@ def test_dispatcher_roster_hosts_pii_blocker() -> None:
370
378
  if each_entry.script_relative_path == "blocking/pii_prevention_blocker.py"
371
379
  ]
372
380
  assert len(matching_bash_entries) == 1
373
- assert (
374
- matching_bash_entries[0].applicable_tool_names
375
- == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
376
- )
381
+ assert matching_bash_entries[0].applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
377
382
 
378
383
 
379
384
  def test_is_git_commit_shell_command_covers_windows_and_flag_forms() -> None:
@@ -531,9 +536,7 @@ def test_is_git_commit_shell_command_detects_glued_and_newline_separators() -> N
531
536
  assert is_git_commit_shell_command("cd repo&&git commit -m x")
532
537
  assert is_git_commit_shell_command("git add notes.md\ngit commit -m update")
533
538
  assert is_git_commit_shell_command("git status\ngit commit -m x")
534
- assert not is_git_commit_shell_command(
535
- "gh pr comment 1 --body 'notes\ngit commit -m x'"
536
- )
539
+ assert not is_git_commit_shell_command("gh pr comment 1 --body 'notes\ngit commit -m x'")
537
540
 
538
541
 
539
542
  def test_is_git_commit_shell_command_detects_backgrounded_commit() -> None:
@@ -681,11 +684,9 @@ def test_argument_bearing_wrapper_commit_scans_staged_pii(tmp_path: Path) -> Non
681
684
 
682
685
 
683
686
  def test_is_git_commit_shell_command_detects_commit_after_hash_bearing_token() -> None:
684
- assert is_git_commit_shell_command(
685
- "curl https://example.com/page#section && git commit -am x"
686
- )
687
+ command = f"curl https://example.com/page{HASH_MARKER}section && git commit -am x"
688
+ assert is_git_commit_shell_command(command)
687
689
  assert is_git_commit_shell_command("echo done#note && git commit -m x")
688
- assert is_git_commit_shell_command('git commit -m "fix #123"')
689
690
 
690
691
 
691
692
  def test_hash_bearing_token_before_commit_scans_staged_pii(tmp_path: Path) -> None:
@@ -721,21 +722,13 @@ def test_is_git_commit_shell_command_detects_unquoted_powershell_command() -> No
721
722
 
722
723
 
723
724
  def test_is_git_commit_shell_command_detects_powershell_preflags_before_command() -> None:
724
- assert is_git_commit_shell_command(
725
- 'pwsh -ExecutionPolicy Bypass -Command "git commit -m x"'
726
- )
727
- assert is_git_commit_shell_command(
728
- 'pwsh -NonInteractive -Command "git commit -m x"'
729
- )
725
+ assert is_git_commit_shell_command('pwsh -ExecutionPolicy Bypass -Command "git commit -m x"')
726
+ assert is_git_commit_shell_command('pwsh -NonInteractive -Command "git commit -m x"')
730
727
  assert is_git_commit_shell_command(
731
728
  'powershell -ExecutionPolicy Bypass -Command "git commit -m x"'
732
729
  )
733
- assert is_git_commit_shell_command(
734
- 'pwsh -NoProfile -NonInteractive -Command "git commit -m x"'
735
- )
736
- assert not is_git_commit_shell_command(
737
- 'pwsh -NonInteractive -Command "git status"'
738
- )
730
+ assert is_git_commit_shell_command('pwsh -NoProfile -NonInteractive -Command "git commit -m x"')
731
+ assert not is_git_commit_shell_command('pwsh -NonInteractive -Command "git status"')
739
732
 
740
733
 
741
734
  def test_is_git_commit_shell_command_detects_subshell_grouped_commit() -> None:
@@ -835,9 +828,7 @@ def test_is_git_commit_shell_command_detects_commit_after_apostrophe_line() -> N
835
828
 
836
829
 
837
830
  def test_is_git_commit_shell_command_ignores_commit_inside_quoted_body() -> None:
838
- assert not is_git_commit_shell_command(
839
- "gh pr comment 1 --body 'notes\ngit commit -m x'"
840
- )
831
+ assert not is_git_commit_shell_command("gh pr comment 1 --body 'notes\ngit commit -m x'")
841
832
 
842
833
 
843
834
  def test_apostrophe_line_before_commit_scans_staged_pii(tmp_path: Path) -> None:
@@ -873,17 +864,9 @@ def test_powershell_backtick_continuation_commit_scans_staged_pii(
873
864
 
874
865
 
875
866
  def test_extract_git_commit_working_directory_resolves_dash_c_forms() -> None:
876
- assert (
877
- extract_git_commit_working_directory(r"git.exe -C /repoA commit -m x")
878
- == "/repoA"
879
- )
880
- assert (
881
- extract_git_commit_working_directory(r"git -C /repoA commit -m x") == "/repoA"
882
- )
883
- assert (
884
- extract_git_commit_working_directory(r'git -C "C:/repo" commit -m x')
885
- == "C:/repo"
886
- )
867
+ assert extract_git_commit_working_directory(r"git.exe -C /repoA commit -m x") == "/repoA"
868
+ assert extract_git_commit_working_directory(r"git -C /repoA commit -m x") == "/repoA"
869
+ assert extract_git_commit_working_directory(r'git -C "C:/repo" commit -m x') == "C:/repo"
887
870
  assert extract_git_commit_working_directory("git commit -m x") is None
888
871
 
889
872
 
@@ -921,9 +904,7 @@ def test_exempt_repository_commit_skips_staged_pii_scan(
921
904
  ) -> None:
922
905
  repository_root = tmp_path / "repo"
923
906
  _init_repo_with_staged_email(repository_root)
924
- _add_repository_origin(
925
- repository_root, "https://github.com/ExemptOwner/exempt-repo.git"
926
- )
907
+ _add_repository_origin(repository_root, "https://github.com/ExemptOwner/exempt-repo.git")
927
908
  monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
928
909
  deny_reason = evaluate_bash_command(
929
910
  "git commit -m test",
@@ -937,9 +918,7 @@ def test_exempt_repository_matches_ssh_origin_url(
937
918
  ) -> None:
938
919
  repository_root = tmp_path / "repo"
939
920
  _init_repo_with_staged_email(repository_root)
940
- _add_repository_origin(
941
- repository_root, "git@github.com:ExemptOwner/exempt-repo.git"
942
- )
921
+ _add_repository_origin(repository_root, "git@github.com:ExemptOwner/exempt-repo.git")
943
922
  monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "exemptowner/EXEMPT-REPO")
944
923
  deny_reason = evaluate_bash_command(
945
924
  "git commit -m test",
@@ -953,9 +932,7 @@ def test_non_exempt_repository_commit_still_scans_staged_pii(
953
932
  ) -> None:
954
933
  repository_root = tmp_path / "repo"
955
934
  _init_repo_with_staged_email(repository_root)
956
- _add_repository_origin(
957
- repository_root, "https://github.com/OtherOwner/other-repo.git"
958
- )
935
+ _add_repository_origin(repository_root, "https://github.com/OtherOwner/other-repo.git")
959
936
  monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
960
937
  deny_reason = evaluate_bash_command(
961
938
  "git commit -m test",
@@ -981,45 +958,35 @@ def test_repository_without_origin_is_not_exempt(
981
958
 
982
959
  def test_owner_repo_slug_from_https_origin_with_git_suffix() -> None:
983
960
  assert (
984
- blocker_module._owner_repo_slug_from_origin_url(
985
- "https://github.com/Owner/repo.git"
986
- )
961
+ blocker_module._owner_repo_slug_from_origin_url("https://github.com/Owner/repo.git")
987
962
  == "owner/repo"
988
963
  )
989
964
 
990
965
 
991
966
  def test_owner_repo_slug_from_https_origin_without_git_suffix() -> None:
992
967
  assert (
993
- blocker_module._owner_repo_slug_from_origin_url(
994
- "https://github.com/Owner/repo"
995
- )
968
+ blocker_module._owner_repo_slug_from_origin_url("https://github.com/Owner/repo")
996
969
  == "owner/repo"
997
970
  )
998
971
 
999
972
 
1000
973
  def test_owner_repo_slug_from_scp_ssh_origin() -> None:
1001
974
  assert (
1002
- blocker_module._owner_repo_slug_from_origin_url(
1003
- "git@github.com:Owner/repo.git"
1004
- )
975
+ blocker_module._owner_repo_slug_from_origin_url("git@github.com:Owner/repo.git")
1005
976
  == "owner/repo"
1006
977
  )
1007
978
 
1008
979
 
1009
980
  def test_owner_repo_slug_from_ssh_scheme_origin() -> None:
1010
981
  assert (
1011
- blocker_module._owner_repo_slug_from_origin_url(
1012
- "ssh://git@github.com/Owner/repo.git"
1013
- )
982
+ blocker_module._owner_repo_slug_from_origin_url("ssh://git@github.com/Owner/repo.git")
1014
983
  == "owner/repo"
1015
984
  )
1016
985
 
1017
986
 
1018
987
  def test_owner_repo_slug_from_trailing_slash_git_suffix() -> None:
1019
988
  assert (
1020
- blocker_module._owner_repo_slug_from_origin_url(
1021
- "https://github.com/Owner/repo.git/"
1022
- )
989
+ blocker_module._owner_repo_slug_from_origin_url("https://github.com/Owner/repo.git/")
1023
990
  == "owner/repo"
1024
991
  )
1025
992
 
@@ -1035,9 +1002,7 @@ def test_owner_repo_slug_rejects_spoofed_evil_host() -> None:
1035
1002
 
1036
1003
  def test_owner_repo_slug_rejects_notgithub_com_host() -> None:
1037
1004
  assert (
1038
- blocker_module._owner_repo_slug_from_origin_url(
1039
- "https://notgithub.com/Owner/repo.git"
1040
- )
1005
+ blocker_module._owner_repo_slug_from_origin_url("https://notgithub.com/Owner/repo.git")
1041
1006
  is None
1042
1007
  )
1043
1008
 
@@ -1071,28 +1036,21 @@ def test_owner_repo_slug_rejects_invalid_ssh_port_authority() -> None:
1071
1036
 
1072
1037
  def test_owner_repo_slug_from_https_origin_with_valid_port() -> None:
1073
1038
  assert (
1074
- blocker_module._owner_repo_slug_from_origin_url(
1075
- "https://github.com:443/Owner/repo.git"
1076
- )
1039
+ blocker_module._owner_repo_slug_from_origin_url("https://github.com:443/Owner/repo.git")
1077
1040
  == "owner/repo"
1078
1041
  )
1079
1042
 
1080
1043
 
1081
1044
  def test_owner_repo_slug_from_ssh_origin_with_valid_port() -> None:
1082
1045
  assert (
1083
- blocker_module._owner_repo_slug_from_origin_url(
1084
- "ssh://git@github.com:22/Owner/repo.git"
1085
- )
1046
+ blocker_module._owner_repo_slug_from_origin_url("ssh://git@github.com:22/Owner/repo.git")
1086
1047
  == "owner/repo"
1087
1048
  )
1088
1049
 
1089
1050
 
1090
1051
  def test_owner_repo_slug_rejects_path_shaped_origin() -> None:
1091
1052
  assert (
1092
- blocker_module._owner_repo_slug_from_origin_url(
1093
- "C:/repos/ExemptOwner/exempt-repo"
1094
- )
1095
- is None
1053
+ blocker_module._owner_repo_slug_from_origin_url("C:/repos/ExemptOwner/exempt-repo") is None
1096
1054
  )
1097
1055
 
1098
1056
 
@@ -1101,9 +1059,7 @@ def test_spoofed_origin_host_still_denies_staged_email(
1101
1059
  ) -> None:
1102
1060
  repository_root = tmp_path / "repo"
1103
1061
  _init_repo_with_staged_email(repository_root)
1104
- _add_repository_origin(
1105
- repository_root, "https://evil.test/ExemptOwner/exempt-repo.git"
1106
- )
1062
+ _add_repository_origin(repository_root, "https://evil.test/ExemptOwner/exempt-repo.git")
1107
1063
  monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1108
1064
  deny_reason = evaluate_bash_command(
1109
1065
  "git commit -m test",
@@ -1151,9 +1107,7 @@ def test_trailing_slash_git_suffix_origin_still_exempts(
1151
1107
  ) -> None:
1152
1108
  repository_root = tmp_path / "repo"
1153
1109
  _init_repo_with_staged_email(repository_root)
1154
- _add_repository_origin(
1155
- repository_root, "https://github.com/ExemptOwner/exempt-repo.git/"
1156
- )
1110
+ _add_repository_origin(repository_root, "https://github.com/ExemptOwner/exempt-repo.git/")
1157
1111
  monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1158
1112
  deny_reason = evaluate_bash_command(
1159
1113
  "git commit -m test",
@@ -1162,14 +1116,10 @@ def test_trailing_slash_git_suffix_origin_still_exempts(
1162
1116
  assert deny_reason is None
1163
1117
 
1164
1118
 
1165
- def test_ssh_scheme_origin_still_exempts(
1166
- monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1167
- ) -> None:
1119
+ def test_ssh_scheme_origin_still_exempts(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
1168
1120
  repository_root = tmp_path / "repo"
1169
1121
  _init_repo_with_staged_email(repository_root)
1170
- _add_repository_origin(
1171
- repository_root, "ssh://git@github.com/ExemptOwner/exempt-repo.git"
1172
- )
1122
+ _add_repository_origin(repository_root, "ssh://git@github.com/ExemptOwner/exempt-repo.git")
1173
1123
  monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
1174
1124
  deny_reason = evaluate_bash_command(
1175
1125
  "git commit -m test",
@@ -1197,9 +1147,7 @@ def test_post_body_still_scans_when_repository_is_exempt(
1197
1147
  monkeypatch: pytest.MonkeyPatch,
1198
1148
  ) -> None:
1199
1149
  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
- )
1150
+ deny_reason = evaluate_post_body_texts([f"Reach me at {SYNTHETIC_REAL_EMAIL}"])
1203
1151
  assert deny_reason is not None
1204
1152
  assert "email" in deny_reason
1205
1153