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.
- package/.agents/skills/_shared/pr-loop/scripts/durable_post_lint.py +242 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py +106 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint.py +332 -0
- package/.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py +25 -0
- package/.agents/skills/issue-tracker/SKILL.md +6 -0
- package/.agents/skills/pull-request/SKILL.md +218 -0
- package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +61 -0
- package/.agents/skills/pull-request/scripts/pull_request.py +276 -0
- package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +49 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +398 -0
- package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
- package/.agents/skills/test-runner/SKILL.md +69 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
- package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/install.mjs +9 -1
- package/bin/install.prune.test.mjs +90 -0
- package/bin/install.test.mjs +24 -7
- package/hooks/blocking/pii_prevention_blocker.py +32 -51
- package/hooks/blocking/test_bash_post_call_dispatcher.py +5 -28
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
- package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
- package/hooks/gh_post_body_texts.py +195 -0
- package/hooks/hooks.json +0 -15
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
- package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
- package/hooks/observability/test_failure_recorder.py +1 -1
- package/hooks/pyproject.toml +1 -1
- package/hooks/test_gh_post_body_texts.py +79 -0
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +31 -20
- package/rules/gh-cli-conventions.md +23 -13
- package/rules/git-workflow.md +21 -8
- package/scripts/policy_lint/adapter_configuration.py +53 -12
- package/scripts/repository_checks/config/constants.py +0 -5
- package/scripts/tests/test_adapter_configuration.py +54 -0
- package/hooks/_gh_pr_author_swap_utils.py +0 -1225
- package/hooks/blocking/conventional_pr_title_gate.py +0 -443
- package/hooks/blocking/gh_body_arg_blocker.py +0 -176
- package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
- package/hooks/blocking/gh_pr_author_restore.py +0 -105
- package/hooks/blocking/pr_description_writer_gate.py +0 -256
- package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
- package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
- package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
- package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
- package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
- package/hooks/blocking/test_preflight_check.py +0 -343
- package/hooks/blocking/test_test_preflight_check.py +0 -80
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
- package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
- package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
- package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
- package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
- package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
251
|
-
to the session working directory only when the command names none
|
|
252
|
-
removed cwd
|
|
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
|
-
|
|
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
|
|
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
|
|
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 == ["
|
|
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
|
-
"""
|
|
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/
|
|
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/
|
|
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
|
-
|
|
685
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
878
|
-
|
|
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
|
|