claude-dev-env 7.0.0 → 7.1.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 (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +189 -0
  6. package/.agents/skills/session-continuity/SKILL.md +114 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +375 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +78 -0
  20. package/bin/install-session-continuity.test.mjs +42 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
@@ -1,105 +0,0 @@
1
- """Tests for the destructive-command-blocker deny mode.
2
-
3
- The sandbox runs under ``--dangerously-skip-permissions``, which auto-resolves
4
- an ``ask`` decision, so only a hard ``deny`` contains a destructive command.
5
- Setting ``CLAUDE_DESTRUCTIVE_DENY_MODE`` turns the hook's terminal ``ask`` into
6
- a ``deny`` so the sandbox can be contained.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import json
12
- import subprocess
13
- import sys
14
- from pathlib import Path
15
-
16
- _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
17
- if str(_BLOCKING_DIRECTORY) not in sys.path:
18
- sys.path.insert(0, str(_BLOCKING_DIRECTORY))
19
-
20
- SCRIPT_PATH = _BLOCKING_DIRECTORY / "destructive_command_blocker.py"
21
- DISPATCHER_PATH = _BLOCKING_DIRECTORY / "bash_pre_tool_use_dispatcher.py"
22
-
23
- import _path_setup # noqa: E402, F401
24
-
25
- from test_hook_subprocess_support import ( # noqa: E402
26
- build_bash_payload,
27
- run_hook_as_subprocess,
28
- )
29
-
30
-
31
- def _run_hook_with_environment(
32
- hook_script_path: Path,
33
- command: str,
34
- environment_update_by_name: dict[str, str],
35
- temporary_home_directory: Path,
36
- ) -> subprocess.CompletedProcess[str]:
37
- return run_hook_as_subprocess(
38
- hook_script_path=hook_script_path,
39
- payload_text=build_bash_payload(command),
40
- working_directory=temporary_home_directory,
41
- home_directory=temporary_home_directory,
42
- all_environment_names_to_remove=("CLAUDE_DESTRUCTIVE_DENY_MODE",),
43
- environment_updates_by_name=environment_update_by_name,
44
- )
45
-
46
-
47
- def _assert_terminal_response(
48
- completed_hook: subprocess.CompletedProcess[str],
49
- expected_permission_decision: str,
50
- expected_permission_reason: str,
51
- ) -> None:
52
- assert completed_hook.returncode == 0
53
- assert completed_hook.stderr == ""
54
- assert json.loads(completed_hook.stdout) == {
55
- "hookSpecificOutput": {
56
- "hookEventName": "PreToolUse",
57
- "permissionDecision": expected_permission_decision,
58
- "permissionDecisionReason": expected_permission_reason,
59
- }
60
- }
61
-
62
-
63
- def test_rm_rf_denies_when_deny_mode_env_is_set(tmp_path: Path) -> None:
64
- completed_hook = _run_hook_with_environment(
65
- SCRIPT_PATH,
66
- "rm -rf /var/log/myapp",
67
- {"CLAUDE_DESTRUCTIVE_DENY_MODE": "1"},
68
- tmp_path,
69
- )
70
- _assert_terminal_response(
71
- completed_hook,
72
- "deny",
73
- "DESTRUCTIVE: rm -rf (destructive recursive forced delete). "
74
- "Blocked in deny mode.",
75
- )
76
-
77
-
78
- def test_rm_rf_asks_when_deny_mode_env_is_absent(tmp_path: Path) -> None:
79
- completed_hook = _run_hook_with_environment(
80
- SCRIPT_PATH,
81
- "rm -rf /var/log/myapp",
82
- {},
83
- tmp_path,
84
- )
85
- _assert_terminal_response(
86
- completed_hook,
87
- "ask",
88
- "DESTRUCTIVE: rm -rf (destructive recursive forced delete). "
89
- "Requires explicit user approval.",
90
- )
91
-
92
-
93
- def test_dispatcher_short_circuits_on_destructive_deny(tmp_path: Path) -> None:
94
- completed_dispatcher = _run_hook_with_environment(
95
- DISPATCHER_PATH,
96
- "rm -rf /var/log/myapp",
97
- {"CLAUDE_DESTRUCTIVE_DENY_MODE": "1"},
98
- tmp_path,
99
- )
100
- _assert_terminal_response(
101
- completed_dispatcher,
102
- "deny",
103
- "DESTRUCTIVE: rm -rf (destructive recursive forced delete). "
104
- "Blocked in deny mode.",
105
- )
@@ -1,122 +0,0 @@
1
- """Tests for --no-verify / --no-gpg-sign blocking in destructive_command_blocker.
2
-
3
- git-workflow.md:30-33 marks these as NON-NEGOTIABLE to skip — they bypass
4
- hook signing and verification. The blocker must ASK before allowing them.
5
- """
6
-
7
- import json
8
- import sys
9
- from pathlib import Path
10
-
11
- _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
12
- if str(_BLOCKING_DIRECTORY) not in sys.path:
13
- sys.path.insert(0, str(_BLOCKING_DIRECTORY))
14
-
15
- import _path_setup # noqa: E402, F401
16
-
17
- from test_hook_subprocess_support import ( # noqa: E402
18
- build_bash_payload,
19
- run_hook_as_subprocess,
20
- )
21
-
22
- SCRIPT_PATH = _BLOCKING_DIRECTORY / "destructive_command_blocker.py"
23
-
24
-
25
- def test_asks_on_git_commit_no_verify(tmp_path: Path) -> None:
26
- result = run_hook_as_subprocess(
27
- hook_script_path=SCRIPT_PATH,
28
- payload_text=build_bash_payload('git commit -m "wip" --no-verify'),
29
- working_directory=tmp_path,
30
- home_directory=tmp_path,
31
- )
32
- response = json.loads(result.stdout)
33
- assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
34
- f"Expected ask for git commit --no-verify, got: {response!r}"
35
- )
36
- assert "no-verify" in response["hookSpecificOutput"]["permissionDecisionReason"], (
37
- f"Reason must mention --no-verify, got: {response!r}"
38
- )
39
-
40
-
41
- def test_asks_on_git_push_no_verify(tmp_path: Path) -> None:
42
- result = run_hook_as_subprocess(
43
- hook_script_path=SCRIPT_PATH,
44
- payload_text=build_bash_payload("git push --no-verify origin main"),
45
- working_directory=tmp_path,
46
- home_directory=tmp_path,
47
- )
48
- response = json.loads(result.stdout)
49
- assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
50
- f"Expected ask for git push --no-verify, got: {response!r}"
51
- )
52
-
53
-
54
- def test_asks_on_git_no_gpg_sign(tmp_path: Path) -> None:
55
- result = run_hook_as_subprocess(
56
- hook_script_path=SCRIPT_PATH,
57
- payload_text=build_bash_payload("git commit --no-gpg-sign -m wip"),
58
- working_directory=tmp_path,
59
- home_directory=tmp_path,
60
- )
61
- response = json.loads(result.stdout)
62
- assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
63
- f"Expected ask for git --no-gpg-sign, got: {response!r}"
64
- )
65
- assert (
66
- "no-gpg-sign" in response["hookSpecificOutput"]["permissionDecisionReason"]
67
- ), f"Reason must mention --no-gpg-sign, got: {response!r}"
68
-
69
-
70
- def test_asks_on_git_commit_with_no_gpg_sign_config(tmp_path: Path) -> None:
71
- result = run_hook_as_subprocess(
72
- hook_script_path=SCRIPT_PATH,
73
- payload_text=build_bash_payload("git -c commit.gpgsign=false commit -m wip"),
74
- working_directory=tmp_path,
75
- home_directory=tmp_path,
76
- )
77
- response = json.loads(result.stdout)
78
- assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
79
- f"Expected ask for -c commit.gpgsign=false, got: {response!r}"
80
- )
81
-
82
-
83
- def test_asks_on_quoted_gpgsign_config(tmp_path: Path) -> None:
84
- result = run_hook_as_subprocess(
85
- hook_script_path=SCRIPT_PATH,
86
- payload_text=build_bash_payload("git -c 'commit.gpgsign=false' commit -m wip"),
87
- working_directory=tmp_path,
88
- home_directory=tmp_path,
89
- )
90
- response = json.loads(result.stdout)
91
- assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
92
- f"Expected ask for quoted -c commit.gpgsign=false, got: {response!r}"
93
- )
94
-
95
-
96
- def test_asks_on_value_quoted_gpgsign_config(tmp_path: Path) -> None:
97
- result = run_hook_as_subprocess(
98
- hook_script_path=SCRIPT_PATH,
99
- payload_text=build_bash_payload("git -c commit.gpgsign='false' commit -m wip"),
100
- working_directory=tmp_path,
101
- home_directory=tmp_path,
102
- )
103
- response = json.loads(result.stdout)
104
- assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
105
- f"Expected ask for value-quoted -c commit.gpgsign='false', got: {response!r}"
106
- )
107
-
108
-
109
- def test_normal_git_commit_passes(tmp_path: Path) -> None:
110
- result = run_hook_as_subprocess(
111
- hook_script_path=SCRIPT_PATH,
112
- payload_text=build_bash_payload('git commit -m "real commit"'),
113
- working_directory=tmp_path,
114
- home_directory=tmp_path,
115
- )
116
- if not result.stdout.strip():
117
- return
118
- response = json.loads(result.stdout)
119
- decision = response.get("hookSpecificOutput", {}).get("permissionDecision", "allow")
120
- assert decision != "ask", (
121
- f"Normal git commit must not be flagged as destructive, got: {response!r}"
122
- )
@@ -1,133 +0,0 @@
1
- """Production-subprocess coverage for destructive command patterns."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import subprocess
7
- import sys
8
- import tempfile
9
- from pathlib import Path
10
-
11
- import pytest
12
-
13
- _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
14
- if str(_BLOCKING_DIRECTORY) not in sys.path:
15
- sys.path.insert(0, str(_BLOCKING_DIRECTORY))
16
-
17
- import _path_setup # noqa: E402, F401
18
-
19
- from hooks_constants.destructive_command_environment_constants import ( # noqa: E402
20
- DESTRUCTIVE_DENY_MODE_ENV_VAR,
21
- EPHEMERAL_AUTO_ALLOW_DISABLE_ENV_VAR,
22
- )
23
- from test_hook_subprocess_support import ( # noqa: E402
24
- build_bash_payload,
25
- run_hook_as_subprocess,
26
- )
27
-
28
- SCRIPT_PATH = _BLOCKING_DIRECTORY / "destructive_command_blocker.py"
29
- ALL_DESTRUCTIVE_ENVIRONMENT_NAMES = (
30
- DESTRUCTIVE_DENY_MODE_ENV_VAR,
31
- EPHEMERAL_AUTO_ALLOW_DISABLE_ENV_VAR,
32
- )
33
-
34
-
35
- def _run_hook(
36
- command: str,
37
- working_directory: Path,
38
- home_directory: Path,
39
- ) -> subprocess.CompletedProcess[str]:
40
- return run_hook_as_subprocess(
41
- hook_script_path=SCRIPT_PATH,
42
- payload_text=build_bash_payload(command),
43
- working_directory=working_directory,
44
- home_directory=home_directory,
45
- all_environment_names_to_remove=ALL_DESTRUCTIVE_ENVIRONMENT_NAMES,
46
- )
47
-
48
-
49
- @pytest.mark.parametrize(
50
- ("command", "reason_fragment"),
51
- [
52
- ("rm -rf /var/log/myapp", "rm -rf"),
53
- ("rm --recursive --force /var/log/myapp", "rm --recursive --force"),
54
- ("rm -r /", "rm -r on broad path"),
55
- ("mkfs.ext4 /dev/sda1", "mkfs"),
56
- ("dd if=/tmp/disk.img of=/dev/sda", "dd raw disk write"),
57
- ("git reset --hard HEAD~1", "git reset --hard"),
58
- ("git push --force origin main", "git push --force"),
59
- ("git push -f origin main", "git push -f"),
60
- ("git clean -fd", "git clean -fd"),
61
- ("git clean -f", "git clean -f"),
62
- ("psql -c 'DROP TABLE users'", "DROP TABLE"),
63
- ("psql -c 'DROP DATABASE app'", "DROP DATABASE"),
64
- ("psql -c 'TRUNCATE TABLE users'", "TRUNCATE TABLE"),
65
- ("git commit --no-verify", "git --no-verify"),
66
- ("git commit --no-gpg-sign", "git --no-gpg-sign"),
67
- ("git -c commit.gpgsign=false commit", "commit.gpgsign=false"),
68
- ],
69
- )
70
- def test_protected_command_asks_for_approval(
71
- command: str,
72
- reason_fragment: str,
73
- tmp_path: Path,
74
- ) -> None:
75
- filesystem_root = Path(Path.cwd().anchor)
76
- completed_hook = _run_hook(command, filesystem_root, tmp_path)
77
-
78
- hook_decision_payload = json.loads(completed_hook.stdout)
79
- assert completed_hook.returncode == 0
80
- assert completed_hook.stderr == ""
81
- assert hook_decision_payload["hookSpecificOutput"]["hookEventName"] == "PreToolUse"
82
- assert hook_decision_payload["hookSpecificOutput"]["permissionDecision"] == "ask"
83
- assert reason_fragment in hook_decision_payload["hookSpecificOutput"][
84
- "permissionDecisionReason"
85
- ]
86
-
87
-
88
- @pytest.mark.parametrize(
89
- "command",
90
- [
91
- "git status --short",
92
- "rm -rf /tmp/destructive-command-blocker/build",
93
- "rm -rf /project/worktrees/feature/build",
94
- "git push --force origin claude/fix-hook-test",
95
- ],
96
- )
97
- def test_permitted_command_exits_silently(command: str, tmp_path: Path) -> None:
98
- completed_hook = _run_hook(command, tmp_path, tmp_path)
99
-
100
- assert completed_hook.returncode == 0
101
- assert completed_hook.stdout == ""
102
- assert completed_hook.stderr == ""
103
-
104
-
105
- def test_ephemeral_rm_exits_silently_when_parent_disables_auto_allow(
106
- monkeypatch: pytest.MonkeyPatch,
107
- tmp_path: Path,
108
- ) -> None:
109
- monkeypatch.setenv(EPHEMERAL_AUTO_ALLOW_DISABLE_ENV_VAR, "1")
110
-
111
- with tempfile.TemporaryDirectory(
112
- dir=tmp_path,
113
- prefix="destructive command blocker ",
114
- ) as ephemeral_directory_path:
115
- completed_hook = _run_hook(
116
- f'rm -rf "{ephemeral_directory_path}"',
117
- tmp_path,
118
- tmp_path,
119
- )
120
-
121
- assert completed_hook.returncode == 0
122
- assert completed_hook.stdout == ""
123
- assert completed_hook.stderr == ""
124
-
125
-
126
- def test_git_reset_hard_in_ephemeral_project_exits_silently(
127
- tmp_path: Path,
128
- ) -> None:
129
- completed_hook = _run_hook("git reset --hard HEAD~1", tmp_path, tmp_path)
130
-
131
- assert completed_hook.returncode == 0
132
- assert completed_hook.stdout == ""
133
- assert completed_hook.stderr == ""