claude-dev-env 1.58.0 → 1.60.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/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/bin/install.mjs +100 -27
- package/bin/install.test.mjs +133 -1
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +439 -0
- package/hooks/blocking/code_rules_enforcer.py +190 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +58 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/file-global-constants.md +7 -1
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +67 -19
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +234 -42
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/rebase/SKILL.md +2 -4
- package/skills/update/SKILL.md +37 -5
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -256,13 +256,13 @@ def test_rm_rf_asks_when_double_dash_includes_hyphen_prefixed_non_ephemeral_targ
|
|
|
256
256
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
257
257
|
|
|
258
258
|
|
|
259
|
-
def
|
|
259
|
+
def test_rm_rf_allowed_when_compound_with_ampersand_and_absolute_ephemeral_target() -> None:
|
|
260
260
|
payload = _make_bash_payload("rm -rf /tmp/reply && gh pr checks 19")
|
|
261
261
|
|
|
262
262
|
result = _run_rm_hook(payload)
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
assert
|
|
264
|
+
assert result.stdout.strip() == ""
|
|
265
|
+
assert result.returncode == 0
|
|
266
266
|
|
|
267
267
|
|
|
268
268
|
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_double_quoted() -> None:
|
|
@@ -1010,6 +1010,513 @@ def test_git_reset_hard_asks_when_settings_file_is_invalid_json(tmp_path: Path)
|
|
|
1010
1010
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1011
1011
|
|
|
1012
1012
|
|
|
1013
|
+
# --- compound ephemeral rm and quoted-mention guard tests ---
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
def _assert_hook_allows(command: str) -> None:
|
|
1017
|
+
result = _run_rm_hook(_make_bash_payload(command))
|
|
1018
|
+
assert result.stdout.strip() == "", (
|
|
1019
|
+
f"Expected allow (no output) for {command!r}, got: {result.stdout!r}"
|
|
1020
|
+
)
|
|
1021
|
+
assert result.returncode == 0
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def _assert_hook_asks(command: str, expected_reason_fragment: str | None = None) -> None:
|
|
1025
|
+
result = _run_rm_hook(_make_bash_payload(command))
|
|
1026
|
+
response = json.loads(result.stdout)
|
|
1027
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
1028
|
+
f"Expected ask for {command!r}, got: {response!r}"
|
|
1029
|
+
)
|
|
1030
|
+
if expected_reason_fragment is not None:
|
|
1031
|
+
assert (
|
|
1032
|
+
expected_reason_fragment
|
|
1033
|
+
in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
1034
|
+
), f"Reason must mention {expected_reason_fragment!r}, got: {response!r}"
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def test_compound_rm_allowed_when_two_absolute_ephemeral_targets_then_echo() -> None:
|
|
1038
|
+
_assert_hook_allows("rm -rf /tmp/pr136 /tmp/difftest && echo 'cleaned'")
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def test_compound_rm_allowed_when_followed_by_gh_pipeline_and_echo() -> None:
|
|
1042
|
+
_assert_hook_allows('rm -rf /tmp/reply && gh pr checks 19 2>&1 | head -5 && echo "x"')
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def test_compound_rm_allowed_when_followed_by_gh_command() -> None:
|
|
1046
|
+
_assert_hook_allows("rm -rf /tmp/reply && gh pr checks 19")
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def test_compound_rm_asks_when_shred_targets_non_ephemeral_path() -> None:
|
|
1050
|
+
_assert_hook_asks("rm -rf /tmp/x && shred -u /etc/passwd")
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def test_compound_rm_asks_when_truncate_targets_non_ephemeral_path() -> None:
|
|
1054
|
+
_assert_hook_asks("rm -rf /tmp/x && truncate -s0 /etc/passwd")
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def test_compound_rm_asks_when_find_delete_walks_root() -> None:
|
|
1058
|
+
_assert_hook_asks("rm -rf /tmp/x && find / -name secret -delete")
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def test_compound_rm_asks_when_chmod_recursive_targets_non_ephemeral_path() -> None:
|
|
1062
|
+
_assert_hook_asks("rm -rf /tmp/x && chmod -R 000 /etc")
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def test_compound_rm_asks_when_mv_moves_non_ephemeral_path() -> None:
|
|
1066
|
+
_assert_hook_asks("rm -rf /tmp/x && mv /home/user/important /tmp/x2")
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
def test_compound_rm_asks_when_gh_repo_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1070
|
+
_assert_hook_asks("rm -rf /tmp/x && gh repo delete jl-cmd/foo --yes")
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def test_compound_rm_asks_when_git_checkout_discards_worktree_after_ephemeral_rm() -> None:
|
|
1074
|
+
_assert_hook_asks("rm -rf /tmp/x && git checkout -- .")
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def test_compound_rm_asks_when_git_stash_drop_rides_alongside_ephemeral_rm() -> None:
|
|
1078
|
+
_assert_hook_asks("rm -rf /tmp/x && git stash drop")
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def test_compound_rm_asks_when_git_branch_force_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1082
|
+
_assert_hook_asks("rm -rf /tmp/x && git branch -D main")
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def test_compound_rm_asks_when_git_clean_force_rides_alongside_ephemeral_rm() -> None:
|
|
1086
|
+
_assert_hook_asks("rm -rf /tmp/x && git clean -fd")
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
def test_compound_rm_asks_when_git_rm_rides_alongside_ephemeral_rm() -> None:
|
|
1090
|
+
_assert_hook_asks("rm -rf /tmp/x && git rm -rf src")
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def test_compound_rm_asks_when_benign_segment_redirects_into_non_ephemeral_file() -> None:
|
|
1094
|
+
_assert_hook_asks("rm -rf /tmp/x && cat /dev/null > /etc/important.conf")
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def test_compound_rm_asks_when_benign_segment_appends_redirect_to_non_ephemeral_file() -> None:
|
|
1098
|
+
_assert_hook_asks("rm -rf /tmp/x && echo hi >> /etc/important.conf")
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def test_compound_rm_allowed_when_gh_read_only_subcommand_follows_ephemeral_rm() -> None:
|
|
1102
|
+
_assert_hook_allows("rm -rf /tmp/reply && gh pr view 19")
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def test_compound_rm_allowed_when_git_read_only_subcommand_follows_ephemeral_rm() -> None:
|
|
1106
|
+
_assert_hook_allows("rm -rf /tmp/reply && git status")
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def test_compound_rm_asks_when_benign_segment_glues_redirect_to_non_ephemeral_file() -> None:
|
|
1110
|
+
_assert_hook_asks("rm -rf /tmp/x && echo pwned>/etc/passwd")
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def test_compound_rm_asks_when_benign_segment_glues_append_redirect_to_non_ephemeral_file() -> None:
|
|
1114
|
+
_assert_hook_asks("rm -rf /tmp/x && echo hi>>/etc/important.conf")
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
def test_compound_rm_asks_when_benign_segment_uses_fd_prefixed_redirect_to_non_ephemeral_file() -> None:
|
|
1118
|
+
_assert_hook_asks("rm -rf /tmp/x && echo hi 1>/etc/important.conf")
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def test_compound_rm_asks_when_benign_segment_uses_glued_combined_redirect_to_non_ephemeral_file() -> None:
|
|
1122
|
+
_assert_hook_asks("rm -rf /tmp/x && echo hi &>/etc/important.conf")
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def test_compound_rm_asks_when_benign_segment_glues_redirect_directly_to_program() -> None:
|
|
1126
|
+
_assert_hook_asks("rm -rf /tmp/x && cat secret>/etc/important.conf")
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def test_compound_rm_asks_when_sort_writes_output_file_to_non_ephemeral_path() -> None:
|
|
1130
|
+
_assert_hook_asks("rm -rf /tmp/x && sort -o /etc/important.conf /etc/passwd")
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
def test_compound_rm_asks_when_rm_target_glues_redirect_to_non_ephemeral_file() -> None:
|
|
1134
|
+
_assert_hook_asks("rm -rf /tmp/x>/etc/passwd")
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def test_compound_rm_asks_when_rm_target_glues_append_redirect_to_non_ephemeral_file() -> None:
|
|
1138
|
+
_assert_hook_asks("rm -rf /tmp/x>>/etc/passwd")
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def test_compound_rm_asks_when_second_rm_target_glues_redirect_to_non_ephemeral_file() -> None:
|
|
1142
|
+
_assert_hook_asks("rm -rf /tmp/a /tmp/b>/etc/hosts")
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def test_compound_rm_asks_when_git_config_sets_value_after_ephemeral_rm() -> None:
|
|
1146
|
+
_assert_hook_asks("rm -rf /tmp/x && git config --global user.name evil")
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def test_compound_rm_asks_when_git_config_sets_value_equal_to_long_read_only_flag() -> None:
|
|
1150
|
+
_assert_hook_asks("rm -rf /tmp/x && git config core.editor --get")
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def test_compound_rm_asks_when_git_config_sets_value_equal_to_list_read_only_flag() -> None:
|
|
1154
|
+
_assert_hook_asks("rm -rf /tmp/x && git config alias.x --list")
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def test_compound_rm_asks_when_git_config_sets_value_equal_to_short_read_only_flag() -> None:
|
|
1158
|
+
_assert_hook_asks("rm -rf /tmp/x && git config core.pager -l")
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
def test_compound_rm_allowed_when_git_config_get_urlmatch_reads_after_ephemeral_rm() -> None:
|
|
1162
|
+
_assert_hook_allows("rm -rf /tmp/reply && git config --get-urlmatch a b")
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def test_compound_rm_asks_when_git_remote_add_rides_alongside_ephemeral_rm() -> None:
|
|
1166
|
+
_assert_hook_asks("rm -rf /tmp/x && git remote add evil http://e")
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def test_compound_rm_asks_when_git_remote_remove_rides_alongside_ephemeral_rm() -> None:
|
|
1170
|
+
_assert_hook_asks("rm -rf /tmp/x && git remote remove origin")
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def test_compound_rm_asks_when_gh_api_http_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1174
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo -X DELETE")
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
def test_compound_rm_asks_when_gh_api_raw_field_implicit_post_rides_alongside_ephemeral_rm() -> None:
|
|
1178
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api repos/o/r/issues -f title=x")
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def test_compound_rm_asks_when_gh_api_field_file_implicit_post_rides_alongside_ephemeral_rm() -> None:
|
|
1182
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api -F a=@b repos/o/r/comments")
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def test_compound_rm_asks_when_gh_api_input_implicit_post_rides_alongside_ephemeral_rm() -> None:
|
|
1186
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api repos/o/r/x --input body.json")
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
def test_compound_rm_allowed_when_gh_api_field_with_explicit_get_follows_ephemeral_rm() -> None:
|
|
1190
|
+
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo -X GET -f a=b")
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def test_compound_rm_asks_when_gh_api_glued_short_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1194
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo -XDELETE")
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def test_compound_rm_asks_when_gh_api_glued_long_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1198
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo --method=DELETE")
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def test_compound_rm_asks_when_gh_api_glued_short_put_rides_alongside_ephemeral_rm() -> None:
|
|
1202
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo -XPUT")
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def test_compound_rm_asks_when_gh_api_glued_long_patch_lowercase_rides_ephemeral_rm() -> None:
|
|
1206
|
+
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo --method=patch")
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
def test_compound_rm_allowed_when_gh_api_glued_short_get_follows_ephemeral_rm() -> None:
|
|
1210
|
+
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo -XGET")
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
def test_compound_rm_allowed_when_gh_api_glued_long_get_follows_ephemeral_rm() -> None:
|
|
1214
|
+
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo --method=GET")
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def test_compound_rm_asks_when_gh_repo_delete_targets_read_only_verb_named_repo() -> None:
|
|
1218
|
+
_assert_hook_asks("rm -rf /tmp/x && gh repo delete status --yes")
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
def test_compound_rm_asks_when_git_stash_drop_targets_read_only_verb_named_ref() -> None:
|
|
1222
|
+
_assert_hook_asks("rm -rf /tmp/x && git stash drop status")
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
def test_compound_rm_asks_when_git_branch_force_delete_targets_read_only_verb_named_branch() -> None:
|
|
1226
|
+
_assert_hook_asks("rm -rf /tmp/x && git branch -D log")
|
|
1227
|
+
|
|
1228
|
+
|
|
1229
|
+
def test_compound_rm_asks_when_git_checkout_discards_read_only_verb_named_path() -> None:
|
|
1230
|
+
_assert_hook_asks("rm -rf /tmp/x && git checkout -- log")
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
def test_compound_rm_asks_when_git_push_targets_read_only_verb_named_ref() -> None:
|
|
1234
|
+
_assert_hook_asks("rm -rf /tmp/x && git push origin log")
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def test_compound_rm_allowed_when_gh_pr_view_takes_read_only_verb_named_argument() -> None:
|
|
1238
|
+
_assert_hook_allows("rm -rf /tmp/reply && gh pr view status")
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def test_compound_rm_asks_when_git_remote_add_after_verbose_flag_rides_ephemeral_rm() -> None:
|
|
1242
|
+
_assert_hook_asks("rm -rf /tmp/x && git remote -v add evil http://attacker")
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def test_compound_rm_asks_when_git_remote_set_url_after_verbose_flag_rides_ephemeral_rm() -> None:
|
|
1246
|
+
_assert_hook_asks("rm -rf /tmp/x && git remote -v set-url origin http://evil")
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def test_compound_rm_asks_when_stdbuf_separate_output_value_wraps_bash_dash_c() -> None:
|
|
1250
|
+
_assert_hook_asks('stdbuf -o L bash -c "rm -rf /etc"')
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def test_compound_rm_asks_when_stdbuf_long_output_value_wraps_bash_dash_c() -> None:
|
|
1254
|
+
_assert_hook_asks('stdbuf --output L bash -c "rm -rf /etc"')
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def test_compound_rm_asks_when_stdbuf_separate_error_value_wraps_bash_dash_c() -> None:
|
|
1258
|
+
_assert_hook_asks('stdbuf -e 0 bash -c "rm -rf /etc"')
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def test_compound_rm_asks_when_ionice_classdata_name_wraps_bash_dash_c() -> None:
|
|
1262
|
+
_assert_hook_asks('ionice --classdata foo bash -c "rm -rf /etc"')
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def test_compound_rm_allowed_when_git_config_lists_after_ephemeral_rm() -> None:
|
|
1266
|
+
_assert_hook_allows("rm -rf /tmp/reply && git config --list")
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
def test_compound_rm_allowed_when_git_remote_lists_after_ephemeral_rm() -> None:
|
|
1270
|
+
_assert_hook_allows("rm -rf /tmp/reply && git remote -v")
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def test_compound_rm_allowed_when_gh_api_get_follows_ephemeral_rm() -> None:
|
|
1274
|
+
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo")
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
def test_quoted_mention_allowed_when_rm_appears_inside_grep_pattern() -> None:
|
|
1278
|
+
_assert_hook_allows("grep 'rm -rf foo' history.jsonl | tail -5")
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def test_quoted_mention_allowed_when_rm_appears_inside_echo_argument() -> None:
|
|
1282
|
+
_assert_hook_allows('echo "rm -rf x"')
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def test_quoted_mention_allowed_when_rm_appears_inside_git_commit_message() -> None:
|
|
1286
|
+
_assert_hook_allows('git commit -m "rm -rf cleanup"')
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def test_compound_rm_asks_when_single_target_is_non_ephemeral() -> None:
|
|
1290
|
+
_assert_hook_asks("rm -rf /var/log/myapp")
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def test_compound_rm_asks_when_second_rm_segment_targets_non_ephemeral() -> None:
|
|
1294
|
+
_assert_hook_asks("rm -rf /tmp/x && rm -rf /etc")
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def test_compound_rm_asks_when_force_push_rides_alongside_ephemeral_rm() -> None:
|
|
1298
|
+
_assert_hook_asks("rm -rf /tmp/x && git push --force origin main")
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def test_compound_rm_asks_when_git_reset_hard_rides_alongside_ephemeral_rm() -> None:
|
|
1302
|
+
_assert_hook_asks("rm -rf /tmp/x && git reset --hard")
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def test_compound_rm_asks_when_command_substitution_present() -> None:
|
|
1306
|
+
_assert_hook_asks("rm -rf /tmp/x && echo $(whoami)")
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
def test_compound_rm_asks_when_backtick_substitution_present() -> None:
|
|
1310
|
+
_assert_hook_asks("rm -rf /tmp/x && echo `whoami`")
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def test_compound_rm_asks_when_relative_target_without_declared_cwd() -> None:
|
|
1314
|
+
_assert_hook_asks("rm -rf scratch && echo done")
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def test_compound_rm_asks_when_rm_token_is_a_variable_expansion() -> None:
|
|
1318
|
+
_assert_hook_asks("$RM -rf /etc")
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def test_quoted_mention_asks_when_absolute_path_rm_runs_on_non_ephemeral() -> None:
|
|
1322
|
+
_assert_hook_asks("/bin/rm -rf /etc")
|
|
1323
|
+
|
|
1324
|
+
|
|
1325
|
+
def test_quoted_mention_asks_when_sudo_rm_runs_on_non_ephemeral() -> None:
|
|
1326
|
+
_assert_hook_asks("sudo rm -rf /etc")
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def test_quoted_mention_asks_when_backslash_rm_runs_on_non_ephemeral() -> None:
|
|
1330
|
+
_assert_hook_asks(r"\rm -rf /etc")
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
def test_quoted_mention_asks_when_real_force_push_rides_alongside_quoted_rm() -> None:
|
|
1334
|
+
_assert_hook_asks(
|
|
1335
|
+
"grep 'rm -rf' f && git push --force origin main",
|
|
1336
|
+
expected_reason_fragment="git push --force",
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def test_interpreter_execution_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1341
|
+
_assert_hook_asks("bash -c 'rm -rf /etc'")
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def test_interpreter_execution_asks_when_sh_dash_c_runs_quoted_rm() -> None:
|
|
1345
|
+
_assert_hook_asks("sh -c 'rm -rf /home/user/x'")
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def test_interpreter_execution_asks_when_eval_runs_quoted_rm() -> None:
|
|
1349
|
+
_assert_hook_asks("eval 'rm -rf /etc'")
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def test_interpreter_execution_asks_when_ssh_runs_remote_quoted_rm() -> None:
|
|
1353
|
+
_assert_hook_asks("ssh host 'rm -rf /etc'")
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def test_interpreter_execution_asks_when_python_dash_c_runs_quoted_rm() -> None:
|
|
1357
|
+
_assert_hook_asks("""python -c "import os; os.system('rm -rf /etc')\"""")
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
def test_interpreter_execution_asks_when_awk_system_runs_quoted_rm() -> None:
|
|
1361
|
+
_assert_hook_asks("""awk 'BEGIN{system("rm -rf /etc")}'""")
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def test_interpreter_execution_asks_when_gawk_system_runs_quoted_rm() -> None:
|
|
1365
|
+
_assert_hook_asks("""gawk 'BEGIN{system("rm -rf /etc")}'""")
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def test_interpreter_execution_asks_when_make_runs_quoted_rm() -> None:
|
|
1369
|
+
_assert_hook_asks("""make -f - 'rm -rf /etc'""")
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def test_compound_rm_asks_when_awk_segment_runs_quoted_rm_after_ephemeral_rm() -> None:
|
|
1373
|
+
_assert_hook_asks("""rm -rf /tmp/x && awk 'BEGIN{system("rm -rf /etc")}'""")
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def test_interpreter_execution_asks_when_benign_command_precedes_bash_dash_c() -> None:
|
|
1377
|
+
_assert_hook_asks("echo hi && bash -c 'rm -rf /etc'")
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def test_interpreter_execution_asks_when_benign_command_precedes_ssh() -> None:
|
|
1381
|
+
_assert_hook_asks("ls && ssh host 'rm -rf /etc'")
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def test_interpreter_execution_asks_when_benign_command_precedes_eval() -> None:
|
|
1385
|
+
_assert_hook_asks("true; eval 'rm -rf /etc'")
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
def test_interpreter_execution_asks_when_benign_command_precedes_python_dash_c() -> None:
|
|
1389
|
+
_assert_hook_asks(
|
|
1390
|
+
"""echo start && python -c "import os; os.system('rm -rf /etc')\""""
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def test_launcher_execution_asks_when_timeout_wraps_bash_dash_c() -> None:
|
|
1395
|
+
_assert_hook_asks("timeout 5 bash -c 'rm -rf /etc'")
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def test_launcher_execution_asks_when_nohup_wraps_bash_dash_c() -> None:
|
|
1399
|
+
_assert_hook_asks("nohup bash -c 'rm -rf /etc'")
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
def test_launcher_execution_asks_when_nice_wraps_bash_dash_c() -> None:
|
|
1403
|
+
_assert_hook_asks("nice -n 10 bash -c 'rm -rf /etc'")
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def test_launcher_execution_asks_when_stdbuf_wraps_bash_dash_c() -> None:
|
|
1407
|
+
_assert_hook_asks("stdbuf -oL bash -c 'rm -rf /etc'")
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
def test_launcher_execution_asks_when_time_wraps_bash_dash_c() -> None:
|
|
1411
|
+
_assert_hook_asks("time bash -c 'rm -rf /etc'")
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def test_launcher_execution_asks_when_setsid_wraps_bash_dash_c() -> None:
|
|
1415
|
+
_assert_hook_asks("setsid bash -c 'rm -rf /etc'")
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def test_launcher_execution_asks_when_ionice_wraps_bash_dash_c() -> None:
|
|
1419
|
+
_assert_hook_asks("ionice bash -c 'rm -rf /etc'")
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
def test_launcher_execution_asks_when_chrt_wraps_bash_dash_c() -> None:
|
|
1423
|
+
_assert_hook_asks("chrt 1 bash -c 'rm -rf /etc'")
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def test_launcher_execution_asks_when_taskset_wraps_bash_dash_c() -> None:
|
|
1427
|
+
_assert_hook_asks("taskset -c 0 bash -c 'rm -rf /etc'")
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def test_launcher_execution_asks_when_stacked_wrappers_precede_bash_dash_c() -> None:
|
|
1431
|
+
_assert_hook_asks("nice -n 5 timeout 5 bash -c 'rm -rf /etc'")
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def test_launcher_execution_asks_when_timeout_wraps_python_dash_c() -> None:
|
|
1435
|
+
_assert_hook_asks(
|
|
1436
|
+
"""timeout 5 python -c "import os; os.system('rm -rf /etc')\""""
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def test_launcher_execution_asks_when_timeout_wraps_bash_after_ephemeral_rm() -> None:
|
|
1441
|
+
_assert_hook_asks("rm -rf /tmp/x && timeout 5 bash -c 'rm -rf /etc'")
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
def test_launcher_execution_allows_when_timeout_wraps_ephemeral_rm() -> None:
|
|
1445
|
+
_assert_hook_allows("timeout 5 rm -rf /tmp/scratch")
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
def test_launcher_execution_asks_when_taskset_hex_mask_wraps_bash_dash_c() -> None:
|
|
1449
|
+
_assert_hook_asks("taskset 0x1 bash -c 'rm -rf /etc'")
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def test_launcher_execution_asks_when_taskset_hex_mask_absolute_path_wraps_bash_dash_c() -> None:
|
|
1453
|
+
_assert_hook_asks("/usr/bin/taskset 0xff bash -c 'rm -rf /etc'")
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def test_launcher_execution_asks_when_taskset_cpu_range_wraps_bash_dash_c() -> None:
|
|
1457
|
+
_assert_hook_asks("taskset -c 0-3 bash -c 'rm -rf /etc'")
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
def test_launcher_execution_asks_when_timeout_duration_suffix_wraps_bash_dash_c() -> None:
|
|
1461
|
+
_assert_hook_asks("timeout 5s bash -c 'rm -rf /etc'")
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def test_launcher_execution_asks_when_taskset_hex_mask_wraps_bash_after_ephemeral_rm() -> None:
|
|
1465
|
+
_assert_hook_asks("rm -rf /tmp/x && timeout 5s bash -c 'rm -rf /etc'")
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def test_newline_separated_interpreter_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1469
|
+
_assert_hook_asks("echo safe\nbash -c 'rm -rf /etc'")
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def test_carriage_return_separated_interpreter_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1473
|
+
_assert_hook_asks("echo hi\rbash -c 'rm -rf /etc'")
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def test_brace_group_newline_interpreter_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1477
|
+
_assert_hook_asks("{ echo hi\nbash -c 'rm -rf /etc'; }")
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def test_launcher_execution_asks_when_timeout_separate_signal_value_wraps_bash_dash_c() -> None:
|
|
1481
|
+
_assert_hook_asks("timeout -s KILL 5 bash -c 'rm -rf /etc'")
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def test_launcher_execution_asks_when_timeout_separate_sigkill_signal_wraps_bash_dash_c() -> None:
|
|
1485
|
+
_assert_hook_asks("timeout -s SIGKILL 5 bash -c 'rm -rf /etc'")
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def test_launcher_execution_asks_when_timeout_long_signal_value_wraps_bash_dash_c() -> None:
|
|
1489
|
+
_assert_hook_asks("timeout --signal KILL 5 bash -c 'rm -rf /etc'")
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def test_launcher_execution_asks_when_timeout_kill_after_value_wraps_bash_dash_c() -> None:
|
|
1493
|
+
_assert_hook_asks("timeout -k 1 5 bash -c 'rm -rf /etc'")
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def test_launcher_execution_asks_when_timeout_separate_signal_wraps_bash_after_ephemeral_rm() -> None:
|
|
1497
|
+
_assert_hook_asks("rm -rf /tmp/x && timeout -s KILL 5 bash -c 'rm -rf /etc'")
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
def test_subshell_grouped_rm_asks_when_parenthesis_glued_to_rm() -> None:
|
|
1501
|
+
_assert_hook_asks("(rm -rf /etc)")
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def test_brace_grouped_rm_asks_when_brace_glued_to_rm() -> None:
|
|
1505
|
+
_assert_hook_asks("{ rm -rf /etc; }")
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def test_glued_semicolon_rm_asks_when_semicolon_prefixes_rm() -> None:
|
|
1509
|
+
_assert_hook_asks(";rm -rf /etc")
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
def test_glued_pipe_rm_asks_when_pipe_joins_echo_to_rm() -> None:
|
|
1513
|
+
_assert_hook_asks("echo|rm -rf /etc")
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
def test_subshell_grouped_rm_asks_when_benign_command_precedes_grouped_rm() -> None:
|
|
1517
|
+
_assert_hook_asks("echo hi; (rm -rf /etc)")
|
|
1518
|
+
|
|
1519
|
+
|
|
1013
1520
|
# --- convergence branch exemption unit tests ---
|
|
1014
1521
|
|
|
1015
1522
|
import importlib.util
|
|
@@ -1026,6 +1533,7 @@ _hook_spec.loader.exec_module(_hook_module)
|
|
|
1026
1533
|
_force_push_targets_convergence_branch = _hook_module._force_push_targets_convergence_branch
|
|
1027
1534
|
_is_convergence_branch = _hook_module._is_convergence_branch
|
|
1028
1535
|
_all_refspecs_are_convergence_branches = _hook_module._all_refspecs_are_convergence_branches
|
|
1536
|
+
_find_non_force_push_destructive_hazard = _hook_module._find_non_force_push_destructive_hazard
|
|
1029
1537
|
|
|
1030
1538
|
|
|
1031
1539
|
def test_convergence_branch_claude_prefix_allowed() -> None:
|
|
@@ -1154,3 +1662,114 @@ def test_force_push_convergence_with_no_gpg_sign_blocked() -> None:
|
|
|
1154
1662
|
response = json.loads(result.stdout)
|
|
1155
1663
|
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1156
1664
|
assert "--no-gpg-sign" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
def test_force_push_convergence_allowed_when_quoted_rm_mention_precedes_push() -> None:
|
|
1668
|
+
_assert_hook_allows('echo "rm -rf foo" && git push --force origin claude/fix-123')
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
def test_force_push_main_asks_when_quoted_rm_mention_precedes_push() -> None:
|
|
1672
|
+
_assert_hook_asks(
|
|
1673
|
+
'echo "rm -rf foo" && git push --force origin main',
|
|
1674
|
+
expected_reason_fragment="git push --force",
|
|
1675
|
+
)
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def test_compound_rm_asks_when_pipe_both_joins_tee_to_non_ephemeral_file() -> None:
|
|
1679
|
+
_assert_hook_asks("rm -rf /tmp/x && cat foo |& tee /etc/passwd")
|
|
1680
|
+
|
|
1681
|
+
|
|
1682
|
+
def test_compound_rm_asks_when_glued_pipe_both_joins_tee_to_non_ephemeral_file() -> None:
|
|
1683
|
+
_assert_hook_asks("rm -rf /tmp/x && cat foo|&tee /etc/passwd")
|
|
1684
|
+
|
|
1685
|
+
|
|
1686
|
+
def test_compound_rm_asks_when_pipe_both_joins_tee_append_to_non_ephemeral_file() -> None:
|
|
1687
|
+
_assert_hook_asks("rm -rf /tmp/x && cat foo |& tee -a /etc/passwd")
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
def test_find_non_force_push_hazard_detects_tee_after_pipe_both_operator() -> None:
|
|
1691
|
+
hazard_description = _find_non_force_push_destructive_hazard(
|
|
1692
|
+
"git push --force origin claude/x && cat foo |& tee /etc/passwd"
|
|
1693
|
+
)
|
|
1694
|
+
assert hazard_description is None or "rm" not in hazard_description
|
|
1695
|
+
assert not _hook_module.rm_compound_targets_only_absolute_ephemeral_paths(
|
|
1696
|
+
"rm -rf /tmp/x && cat foo |& tee /etc/passwd"
|
|
1697
|
+
)
|
|
1698
|
+
|
|
1699
|
+
|
|
1700
|
+
def test_compound_rm_asks_when_glued_semicolon_hides_shred_segment() -> None:
|
|
1701
|
+
_assert_hook_asks("rm -rf /tmp/x && echo hi;shred -u /etc/passwd")
|
|
1702
|
+
|
|
1703
|
+
|
|
1704
|
+
def test_compound_rm_asks_when_glued_and_operator_hides_rm_on_non_ephemeral() -> None:
|
|
1705
|
+
_assert_hook_asks("rm -rf /tmp/x && echo a&&rm -rf /etc")
|
|
1706
|
+
|
|
1707
|
+
|
|
1708
|
+
def test_compound_rm_asks_when_glued_semicolon_hides_interpreter_running_rm() -> None:
|
|
1709
|
+
_assert_hook_asks("rm -rf /tmp/x && echo a;bash -c 'rm -rf /etc'")
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
def test_compound_rm_asks_when_glued_semicolon_hides_gh_repo_delete() -> None:
|
|
1713
|
+
_assert_hook_asks("rm -rf /tmp/x && echo a;gh repo delete jl-cmd/foo --yes")
|
|
1714
|
+
|
|
1715
|
+
|
|
1716
|
+
def test_compound_rm_asks_when_glued_semicolon_hides_rm_after_gh_view() -> None:
|
|
1717
|
+
_assert_hook_asks("rm -rf /tmp/reply && gh pr view 1;rm -rf /etc")
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
def test_compound_rm_asks_when_glued_background_operator_hides_shred_segment() -> None:
|
|
1721
|
+
_assert_hook_asks("rm -rf /tmp/x && echo a&shred -u /etc/passwd")
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def test_compound_rm_asks_when_glued_or_operator_hides_rm_on_non_ephemeral() -> None:
|
|
1725
|
+
_assert_hook_asks("rm -rf /tmp/x && echo a||rm -rf /etc")
|
|
1726
|
+
|
|
1727
|
+
|
|
1728
|
+
def test_compound_rm_asks_when_newline_terminator_hides_shred_segment() -> None:
|
|
1729
|
+
_assert_hook_asks("rm -rf /tmp/x && echo a\nshred -u /etc/passwd")
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def test_compound_rm_asks_when_git_fetch_force_refspec_rewrites_local_branch() -> None:
|
|
1733
|
+
_assert_hook_asks(
|
|
1734
|
+
"rm -rf /tmp/x && git fetch origin +refs/heads/main:refs/heads/main"
|
|
1735
|
+
)
|
|
1736
|
+
|
|
1737
|
+
|
|
1738
|
+
def test_compound_rm_asks_when_git_fetch_long_force_flag_rewrites_local_branch() -> None:
|
|
1739
|
+
_assert_hook_asks(
|
|
1740
|
+
"rm -rf /tmp/x && git fetch --force origin refs/heads/main:refs/heads/main"
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
|
|
1744
|
+
def test_compound_rm_asks_when_git_fetch_short_force_flag_rewrites_local_branch() -> None:
|
|
1745
|
+
_assert_hook_asks(
|
|
1746
|
+
"rm -rf /tmp/x && git fetch -f origin refs/heads/main:refs/heads/main"
|
|
1747
|
+
)
|
|
1748
|
+
|
|
1749
|
+
|
|
1750
|
+
def test_compound_rm_allowed_when_plain_git_fetch_follows_ephemeral_rm() -> None:
|
|
1751
|
+
_assert_hook_allows("rm -rf /tmp/reply && git fetch")
|
|
1752
|
+
|
|
1753
|
+
|
|
1754
|
+
def test_compound_rm_allowed_when_git_fetch_origin_branch_follows_ephemeral_rm() -> None:
|
|
1755
|
+
_assert_hook_allows("rm -rf /tmp/reply && git fetch origin main")
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def test_launcher_execution_asks_when_timeout_infinity_wraps_bash_dash_c() -> None:
|
|
1759
|
+
_assert_hook_asks("timeout inf bash -c 'rm -rf /etc'")
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
def test_launcher_execution_asks_when_timeout_millisecond_duration_wraps_bash_dash_c() -> None:
|
|
1763
|
+
_assert_hook_asks("timeout 100ms bash -c 'rm -rf /etc'")
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
def test_launcher_execution_asks_when_nice_then_timeout_infinity_wraps_bash_dash_c() -> None:
|
|
1767
|
+
_assert_hook_asks("nice -n 5 timeout inf bash -c 'rm -rf /etc'")
|
|
1768
|
+
|
|
1769
|
+
|
|
1770
|
+
def test_launcher_execution_allows_when_timeout_infinity_wraps_ephemeral_rm() -> None:
|
|
1771
|
+
_assert_hook_allows("timeout inf rm -rf /tmp/scratch")
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
def test_launcher_execution_allows_when_timeout_seconds_wraps_ephemeral_rm() -> None:
|
|
1775
|
+
_assert_hook_allows("timeout 5 rm -rf /tmp/scratch")
|