claude-dev-env 2.18.0 → 2.20.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/AGENTS.md +4 -4
- package/.agents/skills/_shared/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +119 -0
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
- package/.agents/skills/e-code-review/SKILL.md +12 -1
- package/.agents/skills/e-code-review/reference/fix.md +5 -1
- package/.agents/skills/e-code-review/reference/loop.md +4 -0
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
- package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
- package/.agents/skills/pr-cleanup/SKILL.md +105 -75
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
- package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
- package/.agents/skills/pr-refinement/SKILL.md +31 -0
- package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
- package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
- package/.agents/skills/skill-builder/SKILL.md +1 -0
- package/.agents/skills-archived/bugteam/SKILL.md +1 -1
- package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
- package/.agents/skills-archived/comments/SKILL.md +1 -1
- package/.agents/skills-archived/descriptions/SKILL.md +1 -1
- package/.agents/skills-archived/reviews/SKILL.md +1 -1
- package/AGENTS.md +1 -1
- package/_shared/pr-loop/AGENTS.md +2 -1
- package/_shared/pr-loop/preflight-proposal.md +56 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
- package/bin/ever-shipped-skills.mjs +14 -0
- package/bin/install-constants.mjs +5 -0
- package/bin/install.agents-home.test.mjs +23 -0
- package/bin/install.codex-rules.test.mjs +61 -6
- package/bin/install.mjs +57 -9
- package/bin/install.prune.test.mjs +65 -3
- package/bin/install.test.mjs +202 -2
- package/docs/CODE_RULES.md +6 -0
- package/hooks/advisory/conftest.py +10 -0
- package/hooks/advisory/migration_safety_advisor.py +14 -14
- package/hooks/advisory/refactor_guard.py +250 -144
- package/hooks/advisory/refactor_guard_test_support.py +46 -0
- package/hooks/advisory/test_migration_safety_advisor.py +120 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
- package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
- package/hooks/atomic_file_writer.py +81 -0
- package/hooks/blocking/AGENTS.md +2 -1
- package/hooks/blocking/block_main_commit.py +66 -33
- package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
- package/hooks/blocking/code_rules_blast_radius.py +194 -0
- package/hooks/blocking/code_rules_enforcer.py +12 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
- package/hooks/blocking/destructive_command_blocker.py +8 -7
- package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
- package/hooks/blocking/precommit_code_rules_gate.py +14 -139
- package/hooks/blocking/sensitive_file_protector.py +8 -6
- package/hooks/blocking/session_edit_stage_gate.py +31 -1
- package/hooks/blocking/test_block_main_commit.py +145 -0
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
- package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +309 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
- package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
- package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
- package/hooks/blocking/test_destructive_command_blocker.py +154 -138
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +85 -25
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
- package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
- package/hooks/blocking/test_hook_subprocess_support.py +296 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
- package/hooks/blocking/test_pii_scanner.py +81 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
- package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +234 -0
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
- package/hooks/blocking/test_sensitive_file_protector.py +145 -5
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
- package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
- package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
- package/hooks/blocking/test_test_preflight_check.py +80 -0
- package/hooks/git-hooks/AGENTS.md +1 -1
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
- package/hooks/git-hooks/post_commit.py +160 -51
- package/hooks/git-hooks/pre_commit.py +9 -5
- package/hooks/git-hooks/test_post_commit.py +203 -0
- package/hooks/git-hooks/test_pre_commit.py +2 -2
- package/hooks/git-hooks/test_pre_push.py +57 -0
- package/hooks/hooks_constants/AGENTS.md +3 -1
- package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
- package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
- package/hooks/hooks_constants/blast_radius_constants.py +14 -0
- package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
- package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
- package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
- package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
- package/hooks/json_file_reader.py +22 -0
- package/hooks/observability/test_instructions_loaded_logger.py +54 -0
- package/hooks/pending_sidecars.py +33 -0
- package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
- package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
- package/hooks/test_atomic_file_writer.py +73 -0
- package/hooks/test_json_file_reader.py +24 -0
- package/hooks/test_pending_sidecars.py +25 -0
- package/hooks/validation/mypy_validator.py +213 -80
- package/hooks/validation/test_mypy_validator.py +288 -13
- package/hooks/workflow/auto_formatter.py +225 -93
- package/hooks/workflow/investigation_tracker_reset.py +2 -0
- package/hooks/workflow/test_auto_formatter.py +261 -12
- package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
- package/package.json +1 -1
- package/rules/failure-blast-radius.md +126 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
- /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
- /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
|
@@ -7,8 +7,17 @@ import sys
|
|
|
7
7
|
import tempfile
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
import pytest
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
_BLOCKING_DIRECTORY = Path(__file__).resolve().parent
|
|
13
|
+
if str(_BLOCKING_DIRECTORY) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_BLOCKING_DIRECTORY))
|
|
15
|
+
|
|
16
|
+
import _path_setup # noqa: E402, F401
|
|
17
|
+
|
|
18
|
+
from test_hook_subprocess_support import build_bash_payload # noqa: E402
|
|
19
|
+
|
|
20
|
+
SCRIPT_PATH = _BLOCKING_DIRECTORY / "destructive_command_blocker.py"
|
|
12
21
|
GH_GATE_USER_FACING_PREFIX = "[gh-gate]"
|
|
13
22
|
GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
|
|
14
23
|
GH_REDIRECT_ACTIVE_VALUE = "1"
|
|
@@ -23,7 +32,7 @@ def _posix_style_system_temporary_root() -> str:
|
|
|
23
32
|
|
|
24
33
|
|
|
25
34
|
def _run_hook(
|
|
26
|
-
payload:
|
|
35
|
+
payload: str,
|
|
27
36
|
gh_redirect_active: bool = True,
|
|
28
37
|
) -> subprocess.CompletedProcess[str]:
|
|
29
38
|
child_environment = os.environ.copy()
|
|
@@ -33,7 +42,7 @@ def _run_hook(
|
|
|
33
42
|
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
34
43
|
return subprocess.run(
|
|
35
44
|
[sys.executable, str(SCRIPT_PATH)],
|
|
36
|
-
input=
|
|
45
|
+
input=payload,
|
|
37
46
|
text=True,
|
|
38
47
|
capture_output=True,
|
|
39
48
|
check=False,
|
|
@@ -41,15 +50,8 @@ def _run_hook(
|
|
|
41
50
|
)
|
|
42
51
|
|
|
43
52
|
|
|
44
|
-
def _make_bash_payload(command: str) -> dict:
|
|
45
|
-
return {
|
|
46
|
-
"tool_name": "Bash",
|
|
47
|
-
"tool_input": {"command": command},
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
53
|
def test_denies_gh_issue_comment_as_redirect_duplicate_guard() -> None:
|
|
52
|
-
payload =
|
|
54
|
+
payload = build_bash_payload(
|
|
53
55
|
'gh issue comment 83 --repo jl-cmd/claude-dev-env --body "hello"'
|
|
54
56
|
)
|
|
55
57
|
result = _run_hook(payload)
|
|
@@ -65,7 +67,7 @@ def test_denies_gh_issue_comment_as_redirect_duplicate_guard() -> None:
|
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
def test_denies_gh_pr_comment_as_redirect_duplicate_guard() -> None:
|
|
68
|
-
payload =
|
|
70
|
+
payload = build_bash_payload('gh pr comment 42 --body "ok"')
|
|
69
71
|
result = _run_hook(payload)
|
|
70
72
|
response = json.loads(result.stdout)
|
|
71
73
|
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
@@ -73,7 +75,7 @@ def test_denies_gh_pr_comment_as_redirect_duplicate_guard() -> None:
|
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
def test_denies_gh_pr_review_as_redirect_duplicate_guard() -> None:
|
|
76
|
-
payload =
|
|
78
|
+
payload = build_bash_payload("gh pr review 42 --approve")
|
|
77
79
|
result = _run_hook(payload)
|
|
78
80
|
response = json.loads(result.stdout)
|
|
79
81
|
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
@@ -81,7 +83,7 @@ def test_denies_gh_pr_review_as_redirect_duplicate_guard() -> None:
|
|
|
81
83
|
|
|
82
84
|
|
|
83
85
|
def test_denies_gh_api_post_comment_as_redirect_duplicate_guard() -> None:
|
|
84
|
-
payload =
|
|
86
|
+
payload = build_bash_payload(
|
|
85
87
|
"gh api /repos/owner/name/issues/1/comments -X POST -f body=hello"
|
|
86
88
|
)
|
|
87
89
|
result = _run_hook(payload)
|
|
@@ -90,7 +92,7 @@ def test_denies_gh_api_post_comment_as_redirect_duplicate_guard() -> None:
|
|
|
90
92
|
|
|
91
93
|
|
|
92
94
|
def test_suppresses_output_on_gh_redirect_deny() -> None:
|
|
93
|
-
payload =
|
|
95
|
+
payload = build_bash_payload('gh issue comment 1 --body "x"')
|
|
94
96
|
result = _run_hook(payload)
|
|
95
97
|
response = json.loads(result.stdout)
|
|
96
98
|
assert response["suppressOutput"] is True
|
|
@@ -98,7 +100,7 @@ def test_suppresses_output_on_gh_redirect_deny() -> None:
|
|
|
98
100
|
|
|
99
101
|
|
|
100
102
|
def test_asks_on_rm_rf_still_works() -> None:
|
|
101
|
-
payload =
|
|
103
|
+
payload = build_bash_payload("rm -rf /var/log/myapp")
|
|
102
104
|
result = _run_hook(payload)
|
|
103
105
|
response = json.loads(result.stdout)
|
|
104
106
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
@@ -106,7 +108,7 @@ def test_asks_on_rm_rf_still_works() -> None:
|
|
|
106
108
|
|
|
107
109
|
|
|
108
110
|
def test_asks_on_git_push_force_still_works() -> None:
|
|
109
|
-
payload =
|
|
111
|
+
payload = build_bash_payload("git push --force origin main")
|
|
110
112
|
result = _run_hook(payload)
|
|
111
113
|
response = json.loads(result.stdout)
|
|
112
114
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
@@ -116,21 +118,23 @@ def test_asks_on_git_push_force_still_works() -> None:
|
|
|
116
118
|
|
|
117
119
|
|
|
118
120
|
def test_allows_plain_command_without_destructive_pattern() -> None:
|
|
119
|
-
payload =
|
|
121
|
+
payload = build_bash_payload("ls -la")
|
|
120
122
|
result = _run_hook(payload)
|
|
121
123
|
assert result.stdout.strip() == ""
|
|
122
124
|
assert result.returncode == 0
|
|
123
125
|
|
|
124
126
|
|
|
125
127
|
def test_ignores_non_bash_tool() -> None:
|
|
126
|
-
payload =
|
|
128
|
+
payload = json.dumps(
|
|
129
|
+
{"tool_name": "Read", "tool_input": {"file_path": "/tmp/x"}}
|
|
130
|
+
)
|
|
127
131
|
result = _run_hook(payload)
|
|
128
132
|
assert result.stdout.strip() == ""
|
|
129
133
|
assert result.returncode == 0
|
|
130
134
|
|
|
131
135
|
|
|
132
136
|
def test_gh_issue_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
133
|
-
payload =
|
|
137
|
+
payload = build_bash_payload('gh issue comment 83 --body "hello"')
|
|
134
138
|
|
|
135
139
|
result = _run_hook(payload, gh_redirect_active=False)
|
|
136
140
|
|
|
@@ -139,7 +143,7 @@ def test_gh_issue_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
|
139
143
|
|
|
140
144
|
|
|
141
145
|
def test_gh_pr_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
142
|
-
payload =
|
|
146
|
+
payload = build_bash_payload('gh pr comment 42 --body "ok"')
|
|
143
147
|
|
|
144
148
|
result = _run_hook(payload, gh_redirect_active=False)
|
|
145
149
|
|
|
@@ -148,7 +152,7 @@ def test_gh_pr_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
|
148
152
|
|
|
149
153
|
|
|
150
154
|
def test_gh_pr_review_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
151
|
-
payload =
|
|
155
|
+
payload = build_bash_payload("gh pr review 42 --approve")
|
|
152
156
|
|
|
153
157
|
result = _run_hook(payload, gh_redirect_active=False)
|
|
154
158
|
|
|
@@ -157,7 +161,7 @@ def test_gh_pr_review_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
|
157
161
|
|
|
158
162
|
|
|
159
163
|
def test_gh_api_post_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
160
|
-
payload =
|
|
164
|
+
payload = build_bash_payload(
|
|
161
165
|
"gh api /repos/owner/name/issues/1/comments -X POST -f body=hello"
|
|
162
166
|
)
|
|
163
167
|
|
|
@@ -167,13 +171,13 @@ def test_gh_api_post_comment_is_allowed_when_redirect_env_var_is_unset() -> None
|
|
|
167
171
|
assert result.returncode == 0
|
|
168
172
|
|
|
169
173
|
|
|
170
|
-
def _run_rm_hook(payload:
|
|
174
|
+
def _run_rm_hook(payload: str) -> subprocess.CompletedProcess[str]:
|
|
171
175
|
child_environment = os.environ.copy()
|
|
172
176
|
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
173
177
|
child_environment.pop("CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW", None)
|
|
174
178
|
return subprocess.run(
|
|
175
179
|
[sys.executable, str(SCRIPT_PATH)],
|
|
176
|
-
input=
|
|
180
|
+
input=payload,
|
|
177
181
|
text=True,
|
|
178
182
|
capture_output=True,
|
|
179
183
|
check=False,
|
|
@@ -182,7 +186,7 @@ def _run_rm_hook(payload: dict) -> subprocess.CompletedProcess[str]:
|
|
|
182
186
|
|
|
183
187
|
|
|
184
188
|
def test_rm_rf_asks_when_target_is_non_ephemeral_path() -> None:
|
|
185
|
-
payload =
|
|
189
|
+
payload = build_bash_payload("rm -rf /var/log/myapp")
|
|
186
190
|
|
|
187
191
|
result = _run_rm_hook(payload)
|
|
188
192
|
|
|
@@ -191,7 +195,7 @@ def test_rm_rf_asks_when_target_is_non_ephemeral_path() -> None:
|
|
|
191
195
|
|
|
192
196
|
|
|
193
197
|
def test_rm_rf_allowed_when_target_is_under_tmp_segment() -> None:
|
|
194
|
-
payload =
|
|
198
|
+
payload = build_bash_payload("rm -rf /tmp/some_scratch_dir")
|
|
195
199
|
|
|
196
200
|
result = _run_rm_hook(payload)
|
|
197
201
|
|
|
@@ -199,10 +203,15 @@ def test_rm_rf_allowed_when_target_is_under_tmp_segment() -> None:
|
|
|
199
203
|
assert result.returncode == 0
|
|
200
204
|
|
|
201
205
|
|
|
202
|
-
def test_rm_rf_allowed_when_target_is_under_os_temp_directory(
|
|
203
|
-
|
|
206
|
+
def test_rm_rf_allowed_when_target_is_under_os_temp_directory(
|
|
207
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
208
|
+
) -> None:
|
|
209
|
+
monkeypatch.setenv("TEMP", str(tmp_path))
|
|
210
|
+
monkeypatch.setenv("TMP", str(tmp_path))
|
|
211
|
+
system_temp_subdirectory = tmp_path / "rm_target"
|
|
212
|
+
system_temp_subdirectory.mkdir()
|
|
204
213
|
forward_slash_temp_path = str(system_temp_subdirectory).replace("\\", "/")
|
|
205
|
-
payload =
|
|
214
|
+
payload = build_bash_payload(f"rm -rf {forward_slash_temp_path}")
|
|
206
215
|
|
|
207
216
|
result = _run_rm_hook(payload)
|
|
208
217
|
|
|
@@ -211,7 +220,7 @@ def test_rm_rf_allowed_when_target_is_under_os_temp_directory() -> None:
|
|
|
211
220
|
|
|
212
221
|
|
|
213
222
|
def test_rm_rf_allowed_when_target_is_under_worktrees_segment() -> None:
|
|
214
|
-
payload =
|
|
223
|
+
payload = build_bash_payload("rm -rf /Users/me/repo/worktrees/feature_branch/build")
|
|
215
224
|
|
|
216
225
|
result = _run_rm_hook(payload)
|
|
217
226
|
|
|
@@ -220,7 +229,7 @@ def test_rm_rf_allowed_when_target_is_under_worktrees_segment() -> None:
|
|
|
220
229
|
|
|
221
230
|
|
|
222
231
|
def test_rm_rf_asks_when_target_is_bare_worktrees_directory() -> None:
|
|
223
|
-
payload =
|
|
232
|
+
payload = build_bash_payload("rm -rf /Users/me/repo/worktrees")
|
|
224
233
|
|
|
225
234
|
result = _run_rm_hook(payload)
|
|
226
235
|
|
|
@@ -229,7 +238,7 @@ def test_rm_rf_asks_when_target_is_bare_worktrees_directory() -> None:
|
|
|
229
238
|
|
|
230
239
|
|
|
231
240
|
def test_rm_rf_asks_when_rm_includes_option_with_equals_sign() -> None:
|
|
232
|
-
payload =
|
|
241
|
+
payload = build_bash_payload("rm -rf --files0-from=/tmp/list /tmp/scratch")
|
|
233
242
|
|
|
234
243
|
result = _run_rm_hook(payload)
|
|
235
244
|
|
|
@@ -238,7 +247,7 @@ def test_rm_rf_asks_when_rm_includes_option_with_equals_sign() -> None:
|
|
|
238
247
|
|
|
239
248
|
|
|
240
249
|
def test_rm_rf_allowed_when_both_targets_are_ephemeral() -> None:
|
|
241
|
-
payload =
|
|
250
|
+
payload = build_bash_payload("rm -rf /tmp/first_dir /tmp/second_dir")
|
|
242
251
|
|
|
243
252
|
result = _run_rm_hook(payload)
|
|
244
253
|
|
|
@@ -247,7 +256,7 @@ def test_rm_rf_allowed_when_both_targets_are_ephemeral() -> None:
|
|
|
247
256
|
|
|
248
257
|
|
|
249
258
|
def test_rm_rf_asks_when_any_target_is_non_ephemeral() -> None:
|
|
250
|
-
payload =
|
|
259
|
+
payload = build_bash_payload("rm -rf /tmp/scratch /etc/passwd")
|
|
251
260
|
|
|
252
261
|
result = _run_rm_hook(payload)
|
|
253
262
|
|
|
@@ -260,7 +269,7 @@ def test_rm_rf_asks_when_target_has_nested_temp_segment_not_at_root() -> None:
|
|
|
260
269
|
|
|
261
270
|
|
|
262
271
|
def test_rm_rf_asks_when_double_dash_includes_hyphen_prefixed_non_ephemeral_target() -> None:
|
|
263
|
-
payload =
|
|
272
|
+
payload = build_bash_payload("rm -rf -- /tmp/scratch -non_ephemeral")
|
|
264
273
|
|
|
265
274
|
result = _run_rm_hook(payload)
|
|
266
275
|
|
|
@@ -269,7 +278,7 @@ def test_rm_rf_asks_when_double_dash_includes_hyphen_prefixed_non_ephemeral_targ
|
|
|
269
278
|
|
|
270
279
|
|
|
271
280
|
def test_rm_rf_allowed_when_compound_with_ampersand_and_absolute_ephemeral_target() -> None:
|
|
272
|
-
payload =
|
|
281
|
+
payload = build_bash_payload("rm -rf /tmp/reply && gh pr checks 19")
|
|
273
282
|
|
|
274
283
|
result = _run_rm_hook(payload)
|
|
275
284
|
|
|
@@ -278,7 +287,7 @@ def test_rm_rf_allowed_when_compound_with_ampersand_and_absolute_ephemeral_targe
|
|
|
278
287
|
|
|
279
288
|
|
|
280
289
|
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_double_quoted() -> None:
|
|
281
|
-
payload =
|
|
290
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf .bugteam-tmp')
|
|
282
291
|
|
|
283
292
|
result = _run_rm_hook(payload)
|
|
284
293
|
|
|
@@ -287,7 +296,7 @@ def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_double_quoted
|
|
|
287
296
|
|
|
288
297
|
|
|
289
298
|
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_single_quoted() -> None:
|
|
290
|
-
payload =
|
|
299
|
+
payload = build_bash_payload("cd '/tmp/bugteam_scratch' && rm -rf .bugteam-tmp")
|
|
291
300
|
|
|
292
301
|
result = _run_rm_hook(payload)
|
|
293
302
|
|
|
@@ -296,7 +305,7 @@ def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_single_quoted
|
|
|
296
305
|
|
|
297
306
|
|
|
298
307
|
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_unquoted() -> None:
|
|
299
|
-
payload =
|
|
308
|
+
payload = build_bash_payload("cd /tmp/bugteam_scratch && rm -rf .bugteam-tmp")
|
|
300
309
|
|
|
301
310
|
result = _run_rm_hook(payload)
|
|
302
311
|
|
|
@@ -309,7 +318,7 @@ def test_rm_rf_allowed_when_leading_cd_into_windows_temp_worktree_subdirectory()
|
|
|
309
318
|
_windows_style_system_temporary_root()
|
|
310
319
|
+ r"\bugteam-pr-58-20260424071040\pr-58\worktree"
|
|
311
320
|
)
|
|
312
|
-
payload =
|
|
321
|
+
payload = build_bash_payload(
|
|
313
322
|
f'cd "{windows_style_temp_worktree}" && rm -rf .bugteam-tmp'
|
|
314
323
|
)
|
|
315
324
|
|
|
@@ -320,7 +329,7 @@ def test_rm_rf_allowed_when_leading_cd_into_windows_temp_worktree_subdirectory()
|
|
|
320
329
|
|
|
321
330
|
|
|
322
331
|
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_with_extra_compound_after_rm() -> None:
|
|
323
|
-
payload =
|
|
332
|
+
payload = build_bash_payload(
|
|
324
333
|
'cd "/tmp/bugteam_scratch" && rm -rf .bugteam-tmp && gh pr checks 19'
|
|
325
334
|
)
|
|
326
335
|
|
|
@@ -331,7 +340,7 @@ def test_rm_rf_allowed_when_leading_cd_into_ephemeral_with_extra_compound_after_
|
|
|
331
340
|
|
|
332
341
|
|
|
333
342
|
def test_rm_rf_asks_when_leading_cd_into_ephemeral_with_wildcard_target() -> None:
|
|
334
|
-
payload =
|
|
343
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf *')
|
|
335
344
|
|
|
336
345
|
result = _run_rm_hook(payload)
|
|
337
346
|
|
|
@@ -340,7 +349,7 @@ def test_rm_rf_asks_when_leading_cd_into_ephemeral_with_wildcard_target() -> Non
|
|
|
340
349
|
|
|
341
350
|
|
|
342
351
|
def test_rm_rf_asks_when_cwd_ephemeral_and_relative_target_escapes_via_dotdot() -> None:
|
|
343
|
-
payload =
|
|
352
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf ../../etc')
|
|
344
353
|
|
|
345
354
|
result = _run_rm_hook(payload)
|
|
346
355
|
|
|
@@ -349,7 +358,7 @@ def test_rm_rf_asks_when_cwd_ephemeral_and_relative_target_escapes_via_dotdot()
|
|
|
349
358
|
|
|
350
359
|
|
|
351
360
|
def test_rm_rf_asks_when_rm_uses_files0_from_long_option_with_equals() -> None:
|
|
352
|
-
payload =
|
|
361
|
+
payload = build_bash_payload(
|
|
353
362
|
'cd "/tmp/bugteam_scratch" && rm -rf --files0-from=/etc/passwd'
|
|
354
363
|
)
|
|
355
364
|
|
|
@@ -360,7 +369,7 @@ def test_rm_rf_asks_when_rm_uses_files0_from_long_option_with_equals() -> None:
|
|
|
360
369
|
|
|
361
370
|
|
|
362
371
|
def test_rm_rf_asks_when_rm_uses_unknown_long_option() -> None:
|
|
363
|
-
payload =
|
|
372
|
+
payload = build_bash_payload(
|
|
364
373
|
'cd "/tmp/bugteam_scratch" && rm -rf --nuke /tmp/bugteam_scratch/stuff'
|
|
365
374
|
)
|
|
366
375
|
|
|
@@ -371,7 +380,7 @@ def test_rm_rf_asks_when_rm_uses_unknown_long_option() -> None:
|
|
|
371
380
|
|
|
372
381
|
|
|
373
382
|
def test_rm_rf_asks_when_rm_target_uses_windows_backslash_absolute_path_unquoted() -> None:
|
|
374
|
-
payload =
|
|
383
|
+
payload = build_bash_payload(
|
|
375
384
|
r'cd "/tmp/bugteam_scratch" && rm -rf C:\sensitive\path'
|
|
376
385
|
)
|
|
377
386
|
|
|
@@ -382,10 +391,7 @@ def test_rm_rf_asks_when_rm_target_uses_windows_backslash_absolute_path_unquoted
|
|
|
382
391
|
|
|
383
392
|
|
|
384
393
|
def test_rm_rf_asks_when_relative_target_without_declared_cwd_fails_closed() -> None:
|
|
385
|
-
payload_with_no_cwd =
|
|
386
|
-
"tool_name": "Bash",
|
|
387
|
-
"tool_input": {"command": "rm -rf relative/path"},
|
|
388
|
-
}
|
|
394
|
+
payload_with_no_cwd = build_bash_payload("rm -rf relative/path")
|
|
389
395
|
|
|
390
396
|
result = _run_rm_hook(payload_with_no_cwd)
|
|
391
397
|
|
|
@@ -394,7 +400,7 @@ def test_rm_rf_asks_when_relative_target_without_declared_cwd_fails_closed() ->
|
|
|
394
400
|
|
|
395
401
|
|
|
396
402
|
def test_rm_rf_allowed_when_cwd_ephemeral_and_relative_target_stays_within() -> None:
|
|
397
|
-
payload =
|
|
403
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf ./build')
|
|
398
404
|
|
|
399
405
|
result = _run_rm_hook(payload)
|
|
400
406
|
|
|
@@ -403,13 +409,9 @@ def test_rm_rf_allowed_when_cwd_ephemeral_and_relative_target_stays_within() ->
|
|
|
403
409
|
|
|
404
410
|
|
|
405
411
|
def test_rm_rf_asks_when_tool_input_cwd_is_ephemeral_but_rm_target_is_absolute_non_ephemeral() -> None:
|
|
406
|
-
payload_with_tool_input_cwd =
|
|
407
|
-
"
|
|
408
|
-
|
|
409
|
-
"command": "rm -rf /var/log/myapp",
|
|
410
|
-
"cwd": "/tmp/bugteam_scratch",
|
|
411
|
-
},
|
|
412
|
-
}
|
|
412
|
+
payload_with_tool_input_cwd = build_bash_payload(
|
|
413
|
+
"rm -rf /var/log/myapp", tool_input_cwd="/tmp/bugteam_scratch"
|
|
414
|
+
)
|
|
413
415
|
|
|
414
416
|
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
415
417
|
|
|
@@ -438,7 +440,7 @@ def test_rm_rf_asks_when_cd_ephemeral_but_target_has_nested_tmp_segment_not_at_r
|
|
|
438
440
|
|
|
439
441
|
|
|
440
442
|
def test_git_push_force_asks_when_leading_cd_into_ephemeral_subdirectory() -> None:
|
|
441
|
-
payload =
|
|
443
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && git push --force')
|
|
442
444
|
|
|
443
445
|
result = _run_rm_hook(payload)
|
|
444
446
|
|
|
@@ -448,7 +450,7 @@ def test_git_push_force_asks_when_leading_cd_into_ephemeral_subdirectory() -> No
|
|
|
448
450
|
|
|
449
451
|
|
|
450
452
|
def test_git_clean_force_recursive_asks_when_leading_cd_into_ephemeral_subdirectory() -> None:
|
|
451
|
-
payload =
|
|
453
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && git clean -fd')
|
|
452
454
|
|
|
453
455
|
result = _run_rm_hook(payload)
|
|
454
456
|
|
|
@@ -458,7 +460,7 @@ def test_git_clean_force_recursive_asks_when_leading_cd_into_ephemeral_subdirect
|
|
|
458
460
|
|
|
459
461
|
|
|
460
462
|
def test_rm_rf_plus_git_push_force_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
461
|
-
payload =
|
|
463
|
+
payload = build_bash_payload(
|
|
462
464
|
'cd "/tmp/bugteam_scratch" && rm -rf cache && git push --force'
|
|
463
465
|
)
|
|
464
466
|
|
|
@@ -469,7 +471,7 @@ def test_rm_rf_plus_git_push_force_piggyback_asks_when_leading_cd_into_ephemeral
|
|
|
469
471
|
|
|
470
472
|
|
|
471
473
|
def test_rm_rf_plus_git_clean_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
472
|
-
payload =
|
|
474
|
+
payload = build_bash_payload(
|
|
473
475
|
'cd "/tmp/bugteam_scratch" && rm -rf cache && git clean -fd'
|
|
474
476
|
)
|
|
475
477
|
|
|
@@ -480,7 +482,7 @@ def test_rm_rf_plus_git_clean_piggyback_asks_when_leading_cd_into_ephemeral() ->
|
|
|
480
482
|
|
|
481
483
|
|
|
482
484
|
def test_rm_rf_plus_mkfs_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
483
|
-
payload =
|
|
485
|
+
payload = build_bash_payload(
|
|
484
486
|
'cd "/tmp/bugteam_scratch" && rm -rf cache && mkfs.ext4 /dev/sda1'
|
|
485
487
|
)
|
|
486
488
|
|
|
@@ -491,7 +493,7 @@ def test_rm_rf_plus_mkfs_piggyback_asks_when_leading_cd_into_ephemeral() -> None
|
|
|
491
493
|
|
|
492
494
|
|
|
493
495
|
def test_rm_rf_plus_drop_table_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
494
|
-
payload =
|
|
496
|
+
payload = build_bash_payload(
|
|
495
497
|
'cd "/tmp/bugteam_scratch" && rm -rf cache && psql -c "DROP TABLE users"'
|
|
496
498
|
)
|
|
497
499
|
|
|
@@ -502,7 +504,7 @@ def test_rm_rf_plus_drop_table_piggyback_asks_when_leading_cd_into_ephemeral() -
|
|
|
502
504
|
|
|
503
505
|
|
|
504
506
|
def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_tmp_root() -> None:
|
|
505
|
-
payload =
|
|
507
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf /tmp')
|
|
506
508
|
|
|
507
509
|
result = _run_rm_hook(payload)
|
|
508
510
|
|
|
@@ -511,7 +513,7 @@ def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_tmp_roo
|
|
|
511
513
|
|
|
512
514
|
|
|
513
515
|
def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_worktrees_root() -> None:
|
|
514
|
-
payload =
|
|
516
|
+
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf /worktrees')
|
|
515
517
|
|
|
516
518
|
result = _run_rm_hook(payload)
|
|
517
519
|
|
|
@@ -520,13 +522,9 @@ def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_worktre
|
|
|
520
522
|
|
|
521
523
|
|
|
522
524
|
def test_rm_rf_asks_when_command_fails_shlex_parse_with_unbalanced_quotes() -> None:
|
|
523
|
-
payload_with_tool_input_cwd =
|
|
524
|
-
"
|
|
525
|
-
|
|
526
|
-
"command": 'rm -rf "unclosed_quote',
|
|
527
|
-
"cwd": "/tmp/bugteam_scratch",
|
|
528
|
-
},
|
|
529
|
-
}
|
|
525
|
+
payload_with_tool_input_cwd = build_bash_payload(
|
|
526
|
+
'rm -rf "unclosed_quote', tool_input_cwd="/tmp/bugteam_scratch"
|
|
527
|
+
)
|
|
530
528
|
|
|
531
529
|
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
532
530
|
|
|
@@ -535,7 +533,7 @@ def test_rm_rf_asks_when_command_fails_shlex_parse_with_unbalanced_quotes() -> N
|
|
|
535
533
|
|
|
536
534
|
|
|
537
535
|
def test_rm_rf_asks_when_leading_cd_target_is_non_ephemeral_directory() -> None:
|
|
538
|
-
payload =
|
|
536
|
+
payload = build_bash_payload('cd "/etc" && rm -rf scratch')
|
|
539
537
|
|
|
540
538
|
result = _run_rm_hook(payload)
|
|
541
539
|
|
|
@@ -544,7 +542,7 @@ def test_rm_rf_asks_when_leading_cd_target_is_non_ephemeral_directory() -> None:
|
|
|
544
542
|
|
|
545
543
|
|
|
546
544
|
def test_rm_rf_asks_when_leading_cd_target_is_bare_ephemeral_root() -> None:
|
|
547
|
-
payload =
|
|
545
|
+
payload = build_bash_payload('cd "/tmp" && rm -rf scratch')
|
|
548
546
|
|
|
549
547
|
result = _run_rm_hook(payload)
|
|
550
548
|
|
|
@@ -553,7 +551,7 @@ def test_rm_rf_asks_when_leading_cd_target_is_bare_ephemeral_root() -> None:
|
|
|
553
551
|
|
|
554
552
|
|
|
555
553
|
def test_rm_rf_allowed_when_leading_cd_target_is_git_worktrees_directory() -> None:
|
|
556
|
-
payload =
|
|
554
|
+
payload = build_bash_payload('cd "/Users/me/repo/worktrees" && rm -rf feature')
|
|
557
555
|
|
|
558
556
|
result = _run_rm_hook(payload)
|
|
559
557
|
|
|
@@ -562,7 +560,7 @@ def test_rm_rf_allowed_when_leading_cd_target_is_git_worktrees_directory() -> No
|
|
|
562
560
|
|
|
563
561
|
|
|
564
562
|
def test_rm_rf_asks_when_leading_cd_target_is_relative_path() -> None:
|
|
565
|
-
payload =
|
|
563
|
+
payload = build_bash_payload('cd "./scratch" && rm -rf inner')
|
|
566
564
|
|
|
567
565
|
result = _run_rm_hook(payload)
|
|
568
566
|
|
|
@@ -571,7 +569,7 @@ def test_rm_rf_asks_when_leading_cd_target_is_relative_path() -> None:
|
|
|
571
569
|
|
|
572
570
|
|
|
573
571
|
def test_rm_rf_allowed_for_chat_observed_bugteam_backslash_worktree_scratch_cleanup() -> None:
|
|
574
|
-
payload =
|
|
572
|
+
payload = build_bash_payload(
|
|
575
573
|
'cd "'
|
|
576
574
|
+ _windows_style_system_temporary_root()
|
|
577
575
|
+ r'\bugteam-pr-58-20260424071040\pr-58\worktree" && rm -rf .bugteam-tmp'
|
|
@@ -584,7 +582,7 @@ def test_rm_rf_allowed_for_chat_observed_bugteam_backslash_worktree_scratch_clea
|
|
|
584
582
|
|
|
585
583
|
|
|
586
584
|
def test_rm_rf_allowed_for_chat_observed_bugteam_forward_slash_worktree_scratch_cleanup() -> None:
|
|
587
|
-
payload =
|
|
585
|
+
payload = build_bash_payload(
|
|
588
586
|
'cd "'
|
|
589
587
|
+ _posix_style_system_temporary_root()
|
|
590
588
|
+ '/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf .bugteam-tmp'
|
|
@@ -597,7 +595,7 @@ def test_rm_rf_allowed_for_chat_observed_bugteam_forward_slash_worktree_scratch_
|
|
|
597
595
|
|
|
598
596
|
|
|
599
597
|
def test_rm_rf_allowed_for_chat_observed_bugfix_reply_scratch_file_cleanup() -> None:
|
|
600
|
-
payload =
|
|
598
|
+
payload = build_bash_payload(
|
|
601
599
|
'cd "'
|
|
602
600
|
+ _posix_style_system_temporary_root()
|
|
603
601
|
+ '/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf tmp_reply_loop1-1.md'
|
|
@@ -610,7 +608,7 @@ def test_rm_rf_allowed_for_chat_observed_bugfix_reply_scratch_file_cleanup() ->
|
|
|
610
608
|
|
|
611
609
|
|
|
612
610
|
def test_rm_rf_allowed_for_chat_observed_bugfind_multiple_scratch_files_cleanup() -> None:
|
|
613
|
-
payload =
|
|
611
|
+
payload = build_bash_payload(
|
|
614
612
|
'cd "'
|
|
615
613
|
+ _posix_style_system_temporary_root()
|
|
616
614
|
+ '/bugteam-pr-58-20260424071040/pr-256/worktree" && rm -rf tmp_review_body.md tmp_finding_1.md'
|
|
@@ -623,14 +621,13 @@ def test_rm_rf_allowed_for_chat_observed_bugfind_multiple_scratch_files_cleanup(
|
|
|
623
621
|
|
|
624
622
|
|
|
625
623
|
def test_rm_rf_allowed_via_tool_input_cwd_pointing_at_chat_observed_bugteam_worktree() -> None:
|
|
626
|
-
payload_with_tool_input_cwd =
|
|
627
|
-
"
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
"
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
624
|
+
payload_with_tool_input_cwd = build_bash_payload(
|
|
625
|
+
"rm -rf .bugteam-tmp",
|
|
626
|
+
tool_input_cwd=(
|
|
627
|
+
_posix_style_system_temporary_root()
|
|
628
|
+
+ "/bugteam-pr-58-20260424071040/pr-58/worktree"
|
|
629
|
+
),
|
|
630
|
+
)
|
|
634
631
|
|
|
635
632
|
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
636
633
|
|
|
@@ -639,7 +636,7 @@ def test_rm_rf_allowed_via_tool_input_cwd_pointing_at_chat_observed_bugteam_work
|
|
|
639
636
|
|
|
640
637
|
|
|
641
638
|
def test_rm_rf_allowed_for_chat_observed_absolute_path_in_bugteam_windows_worktree_scratch() -> None:
|
|
642
|
-
payload =
|
|
639
|
+
payload = build_bash_payload(
|
|
643
640
|
'rm -rf "'
|
|
644
641
|
+ _posix_style_system_temporary_root()
|
|
645
642
|
+ '/bugteam-pr-58-20260424071040/pr-58/worktree/.bugteam-tmp"'
|
|
@@ -652,7 +649,7 @@ def test_rm_rf_allowed_for_chat_observed_absolute_path_in_bugteam_windows_worktr
|
|
|
652
649
|
|
|
653
650
|
|
|
654
651
|
def test_rm_rf_asks_when_leading_cd_target_contains_command_substitution_dollar_parenthesis() -> None:
|
|
655
|
-
payload =
|
|
652
|
+
payload = build_bash_payload(
|
|
656
653
|
'cd "/tmp/$(rm -rf ~/.ssh)" && ls'
|
|
657
654
|
)
|
|
658
655
|
|
|
@@ -663,7 +660,7 @@ def test_rm_rf_asks_when_leading_cd_target_contains_command_substitution_dollar_
|
|
|
663
660
|
|
|
664
661
|
|
|
665
662
|
def test_rm_rf_asks_when_leading_cd_target_contains_backtick_command_substitution() -> None:
|
|
666
|
-
payload =
|
|
663
|
+
payload = build_bash_payload(
|
|
667
664
|
'cd "/tmp/`rm -rf ~/.ssh`" && rm -rf .bugteam-tmp'
|
|
668
665
|
)
|
|
669
666
|
|
|
@@ -674,7 +671,7 @@ def test_rm_rf_asks_when_leading_cd_target_contains_backtick_command_substitutio
|
|
|
674
671
|
|
|
675
672
|
|
|
676
673
|
def test_rm_rf_asks_when_leading_cd_target_contains_variable_expansion() -> None:
|
|
677
|
-
payload =
|
|
674
|
+
payload = build_bash_payload(
|
|
678
675
|
'cd "/tmp/$SNEAKY_VAR" && rm -rf .bugteam-tmp'
|
|
679
676
|
)
|
|
680
677
|
|
|
@@ -685,7 +682,7 @@ def test_rm_rf_asks_when_leading_cd_target_contains_variable_expansion() -> None
|
|
|
685
682
|
|
|
686
683
|
|
|
687
684
|
def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_resolve_outside_ephemeral() -> None:
|
|
688
|
-
payload =
|
|
685
|
+
payload = build_bash_payload(
|
|
689
686
|
'cd "/tmp/a""/../../etc" && rm -rf .'
|
|
690
687
|
)
|
|
691
688
|
|
|
@@ -696,7 +693,7 @@ def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_resolve_outside_ephe
|
|
|
696
693
|
|
|
697
694
|
|
|
698
695
|
def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_use_mixed_quotes() -> None:
|
|
699
|
-
payload =
|
|
696
|
+
payload = build_bash_payload(
|
|
700
697
|
"""cd "/tmp/a"'/../../etc' && rm -rf ."""
|
|
701
698
|
)
|
|
702
699
|
|
|
@@ -707,7 +704,7 @@ def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_use_mixed_quotes() -
|
|
|
707
704
|
|
|
708
705
|
|
|
709
706
|
def test_rm_rf_asks_when_target_is_bare_tmp_root() -> None:
|
|
710
|
-
payload =
|
|
707
|
+
payload = build_bash_payload("rm -rf /tmp")
|
|
711
708
|
|
|
712
709
|
result = _run_rm_hook(payload)
|
|
713
710
|
|
|
@@ -716,7 +713,7 @@ def test_rm_rf_asks_when_target_is_bare_tmp_root() -> None:
|
|
|
716
713
|
|
|
717
714
|
|
|
718
715
|
def test_rm_rf_asks_when_target_is_double_slash_tmp_root() -> None:
|
|
719
|
-
payload =
|
|
716
|
+
payload = build_bash_payload("rm -rf //tmp")
|
|
720
717
|
|
|
721
718
|
result = _run_rm_hook(payload)
|
|
722
719
|
|
|
@@ -724,8 +721,13 @@ def test_rm_rf_asks_when_target_is_double_slash_tmp_root() -> None:
|
|
|
724
721
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
725
722
|
|
|
726
723
|
|
|
727
|
-
def test_rm_rf_asks_when_target_is_bare_os_temp_root(
|
|
728
|
-
|
|
724
|
+
def test_rm_rf_asks_when_target_is_bare_os_temp_root(
|
|
725
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
726
|
+
) -> None:
|
|
727
|
+
system_temporary_root = tempfile.gettempdir()
|
|
728
|
+
monkeypatch.setenv("TEMP", system_temporary_root)
|
|
729
|
+
monkeypatch.setenv("TMP", system_temporary_root)
|
|
730
|
+
payload = build_bash_payload(f"rm -rf {system_temporary_root}")
|
|
729
731
|
|
|
730
732
|
result = _run_rm_hook(payload)
|
|
731
733
|
|
|
@@ -734,14 +736,14 @@ def test_rm_rf_asks_when_target_is_bare_os_temp_root() -> None:
|
|
|
734
736
|
|
|
735
737
|
|
|
736
738
|
def test_rm_rf_asks_when_ephemeral_auto_allow_disabled_via_env_var() -> None:
|
|
737
|
-
payload =
|
|
739
|
+
payload = build_bash_payload("rm -rf /tmp/scratch")
|
|
738
740
|
|
|
739
741
|
child_environment = os.environ.copy()
|
|
740
742
|
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
741
743
|
child_environment["CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW"] = "1"
|
|
742
744
|
result = subprocess.run(
|
|
743
745
|
[sys.executable, str(SCRIPT_PATH)],
|
|
744
|
-
input=
|
|
746
|
+
input=payload,
|
|
745
747
|
text=True,
|
|
746
748
|
capture_output=True,
|
|
747
749
|
check=False,
|
|
@@ -753,7 +755,7 @@ def test_rm_rf_asks_when_ephemeral_auto_allow_disabled_via_env_var() -> None:
|
|
|
753
755
|
|
|
754
756
|
|
|
755
757
|
def test_rm_recursive_force_long_flags_allowed_under_tmp() -> None:
|
|
756
|
-
payload =
|
|
758
|
+
payload = build_bash_payload("rm --recursive --force /tmp/long_flag_scratch")
|
|
757
759
|
|
|
758
760
|
result = _run_rm_hook(payload)
|
|
759
761
|
|
|
@@ -762,7 +764,7 @@ def test_rm_recursive_force_long_flags_allowed_under_tmp() -> None:
|
|
|
762
764
|
|
|
763
765
|
|
|
764
766
|
def test_rm_rf_asks_when_quoted_dotdot_traverses_out_of_ephemeral_root() -> None:
|
|
765
|
-
payload =
|
|
767
|
+
payload = build_bash_payload('rm -rf /tmp/".."/etc')
|
|
766
768
|
|
|
767
769
|
result = _run_rm_hook(payload)
|
|
768
770
|
|
|
@@ -771,7 +773,7 @@ def test_rm_rf_asks_when_quoted_dotdot_traverses_out_of_ephemeral_root() -> None
|
|
|
771
773
|
|
|
772
774
|
|
|
773
775
|
def test_rm_rf_allowed_when_quoted_path_is_legitimate_ephemeral() -> None:
|
|
774
|
-
payload =
|
|
776
|
+
payload = build_bash_payload('rm -rf "/tmp/some scratch dir"')
|
|
775
777
|
|
|
776
778
|
result = _run_rm_hook(payload)
|
|
777
779
|
|
|
@@ -780,7 +782,7 @@ def test_rm_rf_allowed_when_quoted_path_is_legitimate_ephemeral() -> None:
|
|
|
780
782
|
|
|
781
783
|
|
|
782
784
|
def test_rm_rf_asks_when_single_quoted_dotdot_traverses_out_of_ephemeral() -> None:
|
|
783
|
-
payload =
|
|
785
|
+
payload = build_bash_payload("rm -rf /tmp/'..'/etc")
|
|
784
786
|
|
|
785
787
|
result = _run_rm_hook(payload)
|
|
786
788
|
|
|
@@ -789,7 +791,7 @@ def test_rm_rf_asks_when_single_quoted_dotdot_traverses_out_of_ephemeral() -> No
|
|
|
789
791
|
|
|
790
792
|
|
|
791
793
|
def test_rm_rf_asks_when_target_is_glob_wildcard_under_tmp() -> None:
|
|
792
|
-
payload =
|
|
794
|
+
payload = build_bash_payload("rm -rf /tmp/*")
|
|
793
795
|
|
|
794
796
|
result = _run_rm_hook(payload)
|
|
795
797
|
|
|
@@ -798,7 +800,7 @@ def test_rm_rf_asks_when_target_is_glob_wildcard_under_tmp() -> None:
|
|
|
798
800
|
|
|
799
801
|
|
|
800
802
|
def test_rm_rf_asks_when_target_is_question_mark_glob_under_tmp() -> None:
|
|
801
|
-
payload =
|
|
803
|
+
payload = build_bash_payload("rm -rf /tmp/?")
|
|
802
804
|
|
|
803
805
|
result = _run_rm_hook(payload)
|
|
804
806
|
|
|
@@ -807,7 +809,7 @@ def test_rm_rf_asks_when_target_is_question_mark_glob_under_tmp() -> None:
|
|
|
807
809
|
|
|
808
810
|
|
|
809
811
|
def test_rm_rf_asks_when_target_is_bracket_glob_under_tmp() -> None:
|
|
810
|
-
payload =
|
|
812
|
+
payload = build_bash_payload("rm -rf /tmp/[abc]")
|
|
811
813
|
|
|
812
814
|
result = _run_rm_hook(payload)
|
|
813
815
|
|
|
@@ -816,7 +818,7 @@ def test_rm_rf_asks_when_target_is_bracket_glob_under_tmp() -> None:
|
|
|
816
818
|
|
|
817
819
|
|
|
818
820
|
def test_rm_rf_asks_when_target_is_worktrees_glob() -> None:
|
|
819
|
-
payload =
|
|
821
|
+
payload = build_bash_payload("rm -rf /worktrees/*")
|
|
820
822
|
|
|
821
823
|
result = _run_rm_hook(payload)
|
|
822
824
|
|
|
@@ -824,10 +826,14 @@ def test_rm_rf_asks_when_target_is_worktrees_glob() -> None:
|
|
|
824
826
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
825
827
|
|
|
826
828
|
|
|
827
|
-
def test_rm_rf_asks_when_target_is_os_temp_root_glob(
|
|
828
|
-
|
|
829
|
+
def test_rm_rf_asks_when_target_is_os_temp_root_glob(
|
|
830
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
831
|
+
) -> None:
|
|
832
|
+
monkeypatch.setenv("TEMP", str(tmp_path))
|
|
833
|
+
monkeypatch.setenv("TMP", str(tmp_path))
|
|
834
|
+
system_temporary_root = str(tmp_path)
|
|
829
835
|
forward_slash_temp_root = system_temporary_root.replace("\\", "/")
|
|
830
|
-
payload =
|
|
836
|
+
payload = build_bash_payload(f"rm -rf {forward_slash_temp_root}/*")
|
|
831
837
|
|
|
832
838
|
result = _run_rm_hook(payload)
|
|
833
839
|
|
|
@@ -835,10 +841,14 @@ def test_rm_rf_asks_when_target_is_os_temp_root_glob() -> None:
|
|
|
835
841
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
836
842
|
|
|
837
843
|
|
|
838
|
-
def test_rm_rf_allowed_when_unquoted_windows_backslash_target_is_ephemeral(
|
|
839
|
-
|
|
844
|
+
def test_rm_rf_allowed_when_unquoted_windows_backslash_target_is_ephemeral(
|
|
845
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
846
|
+
) -> None:
|
|
847
|
+
monkeypatch.setenv("TEMP", str(tmp_path))
|
|
848
|
+
monkeypatch.setenv("TMP", str(tmp_path))
|
|
849
|
+
system_temporary_root = str(tmp_path)
|
|
840
850
|
windows_style_target = system_temporary_root.replace("/", "\\") + "\\scratch"
|
|
841
|
-
payload =
|
|
851
|
+
payload = build_bash_payload(f"rm -rf {windows_style_target}")
|
|
842
852
|
|
|
843
853
|
result = _run_rm_hook(payload)
|
|
844
854
|
|
|
@@ -847,7 +857,7 @@ def test_rm_rf_allowed_when_unquoted_windows_backslash_target_is_ephemeral() ->
|
|
|
847
857
|
|
|
848
858
|
|
|
849
859
|
def test_rm_rf_allowed_when_unquoted_windows_backslash_target_contains_worktrees_segment() -> None:
|
|
850
|
-
payload =
|
|
860
|
+
payload = build_bash_payload(
|
|
851
861
|
r"rm -rf C:\Users\developer\project\worktrees\feature_branch\build"
|
|
852
862
|
)
|
|
853
863
|
|
|
@@ -858,7 +868,7 @@ def test_rm_rf_allowed_when_unquoted_windows_backslash_target_contains_worktrees
|
|
|
858
868
|
|
|
859
869
|
|
|
860
870
|
def test_rm_rf_allowed_when_finding_example_windows_backslash_ephemeral_target() -> None:
|
|
861
|
-
payload =
|
|
871
|
+
payload = build_bash_payload(
|
|
862
872
|
"rm -rf " + _windows_style_system_temporary_root() + r"\scratch"
|
|
863
873
|
)
|
|
864
874
|
|
|
@@ -869,7 +879,7 @@ def test_rm_rf_allowed_when_finding_example_windows_backslash_ephemeral_target()
|
|
|
869
879
|
|
|
870
880
|
|
|
871
881
|
def test_rm_rf_asks_when_target_is_literal_tmp_star_finding_example() -> None:
|
|
872
|
-
payload =
|
|
882
|
+
payload = build_bash_payload("rm -rf /tmp/*")
|
|
873
883
|
|
|
874
884
|
result = _run_rm_hook(payload)
|
|
875
885
|
|
|
@@ -878,7 +888,7 @@ def test_rm_rf_asks_when_target_is_literal_tmp_star_finding_example() -> None:
|
|
|
878
888
|
|
|
879
889
|
|
|
880
890
|
def test_rm_rf_asks_when_target_basename_is_wildcard_with_prefix_under_tmp() -> None:
|
|
881
|
-
payload =
|
|
891
|
+
payload = build_bash_payload("rm -rf /tmp/foo*")
|
|
882
892
|
|
|
883
893
|
result = _run_rm_hook(payload)
|
|
884
894
|
|
|
@@ -887,7 +897,7 @@ def test_rm_rf_asks_when_target_basename_is_wildcard_with_prefix_under_tmp() ->
|
|
|
887
897
|
|
|
888
898
|
|
|
889
899
|
def _run_hook_with_fake_home(
|
|
890
|
-
payload:
|
|
900
|
+
payload: str,
|
|
891
901
|
fake_home: Path,
|
|
892
902
|
working_directory: Path,
|
|
893
903
|
disable_ephemeral_auto_allow: bool = True,
|
|
@@ -902,7 +912,7 @@ def _run_hook_with_fake_home(
|
|
|
902
912
|
child_environment.pop("CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW", None)
|
|
903
913
|
return subprocess.run(
|
|
904
914
|
[sys.executable, str(SCRIPT_PATH)],
|
|
905
|
-
input=
|
|
915
|
+
input=payload,
|
|
906
916
|
text=True,
|
|
907
917
|
capture_output=True,
|
|
908
918
|
check=False,
|
|
@@ -927,7 +937,7 @@ def test_git_reset_hard_allowed_when_cwd_matches_settings_allow_list(tmp_path: P
|
|
|
927
937
|
project_directory.mkdir()
|
|
928
938
|
project_path_forward = str(project_directory).replace("\\", "/")
|
|
929
939
|
_write_settings_with_allow_list(fake_home, [project_path_forward])
|
|
930
|
-
payload =
|
|
940
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
931
941
|
|
|
932
942
|
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
933
943
|
|
|
@@ -944,7 +954,7 @@ def test_git_reset_hard_asks_when_cwd_not_in_settings_allow_list(tmp_path: Path)
|
|
|
944
954
|
unapproved_directory.mkdir()
|
|
945
955
|
approved_path_forward = str(approved_directory).replace("\\", "/")
|
|
946
956
|
_write_settings_with_allow_list(fake_home, [approved_path_forward])
|
|
947
|
-
payload =
|
|
957
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
948
958
|
|
|
949
959
|
result = _run_hook_with_fake_home(payload, fake_home, unapproved_directory)
|
|
950
960
|
|
|
@@ -958,7 +968,7 @@ def test_git_reset_hard_asks_when_settings_missing(tmp_path: Path) -> None:
|
|
|
958
968
|
fake_home.mkdir()
|
|
959
969
|
project_directory = tmp_path / "unapproved_project"
|
|
960
970
|
project_directory.mkdir()
|
|
961
|
-
payload =
|
|
971
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
962
972
|
|
|
963
973
|
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
964
974
|
|
|
@@ -972,7 +982,7 @@ def test_git_reset_hard_asks_when_allow_list_is_empty(tmp_path: Path) -> None:
|
|
|
972
982
|
project_directory = tmp_path / "some_project"
|
|
973
983
|
project_directory.mkdir()
|
|
974
984
|
_write_settings_with_allow_list(fake_home, [])
|
|
975
|
-
payload =
|
|
985
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
976
986
|
|
|
977
987
|
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
978
988
|
|
|
@@ -993,7 +1003,7 @@ def test_git_reset_hard_allowed_in_linked_git_worktree(tmp_path: Path) -> None:
|
|
|
993
1003
|
cwd=main_repository,
|
|
994
1004
|
check=True,
|
|
995
1005
|
)
|
|
996
|
-
payload =
|
|
1006
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
997
1007
|
|
|
998
1008
|
result = _run_hook_with_fake_home(payload, fake_home, worktree_directory, disable_ephemeral_auto_allow=False)
|
|
999
1009
|
|
|
@@ -1006,7 +1016,7 @@ def test_git_reset_hard_allowed_when_path_contains_worktrees_segment(tmp_path: P
|
|
|
1006
1016
|
fake_home.mkdir()
|
|
1007
1017
|
worktree_directory = tmp_path / "worktrees" / "some_feature"
|
|
1008
1018
|
worktree_directory.mkdir(parents=True)
|
|
1009
|
-
payload =
|
|
1019
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
1010
1020
|
|
|
1011
1021
|
result = _run_hook_with_fake_home(payload, fake_home, worktree_directory, disable_ephemeral_auto_allow=False)
|
|
1012
1022
|
|
|
@@ -1014,10 +1024,16 @@ def test_git_reset_hard_allowed_when_path_contains_worktrees_segment(tmp_path: P
|
|
|
1014
1024
|
assert result.returncode == 0
|
|
1015
1025
|
|
|
1016
1026
|
|
|
1017
|
-
def test_git_reset_hard_allowed_under_os_temp_directory(
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1027
|
+
def test_git_reset_hard_allowed_under_os_temp_directory(
|
|
1028
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1029
|
+
) -> None:
|
|
1030
|
+
monkeypatch.setenv("TEMP", str(tmp_path))
|
|
1031
|
+
monkeypatch.setenv("TMP", str(tmp_path))
|
|
1032
|
+
fake_home = tmp_path / "home"
|
|
1033
|
+
fake_home.mkdir()
|
|
1034
|
+
working_directory = tmp_path / "ephemeral_work"
|
|
1035
|
+
working_directory.mkdir()
|
|
1036
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
1021
1037
|
|
|
1022
1038
|
result = _run_hook_with_fake_home(payload, fake_home, working_directory, disable_ephemeral_auto_allow=False)
|
|
1023
1039
|
|
|
@@ -1030,7 +1046,7 @@ def test_git_reset_hard_asks_in_plain_directory_with_no_settings(tmp_path: Path)
|
|
|
1030
1046
|
fake_home.mkdir()
|
|
1031
1047
|
plain_directory = tmp_path / "regular_project"
|
|
1032
1048
|
plain_directory.mkdir()
|
|
1033
|
-
payload =
|
|
1049
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
1034
1050
|
|
|
1035
1051
|
result = _run_hook_with_fake_home(payload, fake_home, plain_directory)
|
|
1036
1052
|
|
|
@@ -1046,7 +1062,7 @@ def test_git_reset_hard_asks_when_settings_file_is_invalid_json(tmp_path: Path)
|
|
|
1046
1062
|
)
|
|
1047
1063
|
project_directory = tmp_path / "unapproved_project"
|
|
1048
1064
|
project_directory.mkdir()
|
|
1049
|
-
payload =
|
|
1065
|
+
payload = build_bash_payload("git reset --hard origin/main")
|
|
1050
1066
|
|
|
1051
1067
|
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
1052
1068
|
|
|
@@ -1058,7 +1074,7 @@ def test_git_reset_hard_asks_when_settings_file_is_invalid_json(tmp_path: Path)
|
|
|
1058
1074
|
|
|
1059
1075
|
|
|
1060
1076
|
def _assert_hook_allows(command: str) -> None:
|
|
1061
|
-
result = _run_rm_hook(
|
|
1077
|
+
result = _run_rm_hook(build_bash_payload(command))
|
|
1062
1078
|
assert result.stdout.strip() == "", (
|
|
1063
1079
|
f"Expected allow (no output) for {command!r}, got: {result.stdout!r}"
|
|
1064
1080
|
)
|
|
@@ -1066,7 +1082,7 @@ def _assert_hook_allows(command: str) -> None:
|
|
|
1066
1082
|
|
|
1067
1083
|
|
|
1068
1084
|
def _assert_hook_asks(command: str, expected_reason_fragment: str | None = None) -> None:
|
|
1069
|
-
result = _run_rm_hook(
|
|
1085
|
+
result = _run_rm_hook(build_bash_payload(command))
|
|
1070
1086
|
response = json.loads(result.stdout)
|
|
1071
1087
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
1072
1088
|
f"Expected ask for {command!r}, got: {response!r}"
|
|
@@ -1721,7 +1737,7 @@ def test_all_refspecs_convergence_branch_with_flags() -> None:
|
|
|
1721
1737
|
|
|
1722
1738
|
|
|
1723
1739
|
def test_force_push_convergence_with_no_verify_blocked() -> None:
|
|
1724
|
-
payload =
|
|
1740
|
+
payload = build_bash_payload(
|
|
1725
1741
|
"git push --force origin --no-verify claude/fix-123"
|
|
1726
1742
|
)
|
|
1727
1743
|
|
|
@@ -1733,7 +1749,7 @@ def test_force_push_convergence_with_no_verify_blocked() -> None:
|
|
|
1733
1749
|
|
|
1734
1750
|
|
|
1735
1751
|
def test_force_push_convergence_with_no_gpg_sign_blocked() -> None:
|
|
1736
|
-
payload =
|
|
1752
|
+
payload = build_bash_payload(
|
|
1737
1753
|
"git push --force origin --no-gpg-sign claude/fix-123"
|
|
1738
1754
|
)
|
|
1739
1755
|
|