claude-dev-env 4.3.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/pr-description-writer.md +2 -2
- package/.agents/agents/test_agent_frontmatter.py +2 -2
- package/.agents/agents/test_pr_description_writer_style.py +1 -3
- package/.agents/skills/e-code-review/SKILL.md +12 -12
- package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
- package/.agents/skills/e-code-review/reference/fix.md +15 -17
- package/.agents/skills/e-code-review/reference/loop.md +35 -39
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
- package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
- package/.agents/skills/issue-tracker/SKILL.md +5 -4
- package/.agents/skills/pull-request/SKILL.md +11 -13
- package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
- package/.agents/skills/pull-request/scripts/pull_request.py +35 -44
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +35 -3
- package/.agents/skills/session-continuity/README.md +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
- package/.agents/skills/skill-builder/SKILL.md +3 -196
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.agents-home.test.mjs +15 -13
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +506 -63
- package/bin/install.policy-lint.test.mjs +130 -0
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +286 -37
- package/bin/install.pstack-plugin.test.mjs +114 -0
- package/bin/install.test.mjs +537 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
- package/docs/CODE_RULES.md +1 -1
- package/hooks/advisory/pr_done_reminder.py +342 -0
- package/hooks/advisory/test_pr_done_reminder.py +249 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/code_rules_paired_test.py +22 -15
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +43 -342
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -49
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +208 -0
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +120 -0
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +38 -148
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +1 -120
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -93
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +46 -201
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +2 -2
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/durable-post-artifacts.md +2 -1
- package/rules/git-workflow.md +18 -6
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/check.ps1 +0 -1
- package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
- package/scripts/ci/installer-lifecycle.test.mjs +26 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
- package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +35 -43
- package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +10 -10
- package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +14 -1
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_detectors.py +13 -6
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/adapters.py +7 -4
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/policy_lint/config/constants.py +5 -0
- package/scripts/policy_lint/engine.py +48 -4
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection.py +2 -1
- package/scripts/policy_lint/selection_git.py +2 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
- package/scripts/shared_tree_paths.py +32 -3
- package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +97 -0
- package/scripts/test_shared_tree_paths.py +33 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
- package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
- package/scripts/tests/test_policy_lint_selection.py +25 -2
- package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_rules.py +3 -7
- package/scripts/tests/test_verification_notice.py +347 -0
- package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
- package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
- package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
- package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
- package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
- package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
- package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
- package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
- package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
- package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
- package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
- package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
- package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
- package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
- package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
- package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
- package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
- package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
- package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
- package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
- package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
- package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
- package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
- package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
- package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
- package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
- package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
- package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
- package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
- package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
- package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
- package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
- package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
- package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
- package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
- package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
- package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/SKILL.md +0 -431
- package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
- package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
- package/.agents/skills/autoconverge/reference/convergence.md +0 -219
- package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
- package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
- package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
- package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
- package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
- package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
- package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
- package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
- package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
- package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
- package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
- package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
- package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
- package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
- package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
- package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
- package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
- package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
- package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
- package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
- package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
- package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
- package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
- package/.agents/skills/hitl/SKILL.md +0 -45
- package/.agents/skills/pr-cleanup/SKILL.md +0 -140
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
- package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
- package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
- package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
- package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
- package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
- package/.agents/skills/pr-refinement/SKILL.md +0 -31
- package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
- package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
- package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
- package/.agents/skills/pr-small-cl/SKILL.md +0 -52
- package/.agents/skills/pr-title-description/SKILL.md +0 -96
- package/.agents/skills/prototype/SKILL.md +0 -85
- package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
- package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
- package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
- package/.agents/skills/prototype/scripts/conftest.py +0 -15
- package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
- package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
- package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
- package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
- package/.agents/skills/prototype/workflows/promotion.md +0 -25
- package/.agents/skills/prototype/workflows/sandbox.md +0 -35
- package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
- package/.agents/skills/rebase/AGENTS.md +0 -1
- package/.agents/skills/rebase/SKILL.md +0 -178
- package/.agents/skills/review-router/SKILL.md +0 -51
- package/.agents/skills/review-router/reference/route-policy.json +0 -7
- package/.agents/skills/review-router/reference/task-seeds.md +0 -1
- package/.agents/skills/review-router/scripts/conftest.py +0 -4
- package/.agents/skills/review-router/scripts/review_router.py +0 -50
- package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
- package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
- package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
- package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
- package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
- package/.agents/skills/review-tier/SKILL.md +0 -56
- package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
- package/.agents/skills/review-tier/scripts/conftest.py +0 -4
- package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
- package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
- package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
- package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
- package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
- package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-log/AGENTS.md +0 -1
- package/.agents/skills/session-log/SKILL.md +0 -170
- package/.agents/skills/session-log/templates/frontmatter.md +0 -40
- package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-tidy/AGENTS.md +0 -1
- package/.agents/skills/session-tidy/SKILL.md +0 -191
- package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
- package/.agents/skills/skill-builder/references/description-field.md +0 -111
- package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
- package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
- package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
- package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
- package/.agents/skills/skill-builder/references/skill-types.md +0 -232
- package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
- package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
- package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
- package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
- package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
- package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
- package/.agents/skills/update/.claude/CLAUDE.md +0 -1
- package/.agents/skills/update/AGENTS.md +0 -1
- package/.agents/skills/update/SKILL.md +0 -190
- package/_shared/pr-loop/AGENTS.md +0 -1
- package/_shared/pr-loop/audit-contract.md +0 -183
- package/_shared/pr-loop/audit-reply-template.md +0 -151
- package/_shared/pr-loop/code-rules-gate.md +0 -65
- package/_shared/pr-loop/fix-protocol.md +0 -60
- package/_shared/pr-loop/gh-payloads.md +0 -75
- package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
- package/_shared/pr-loop/precatch-rubric.md +0 -66
- package/_shared/pr-loop/preflight-proposal.md +0 -56
- package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
- package/_shared/pr-loop/state-schema.md +0 -45
- package/_shared/pr-loop/worker-spawn.md +0 -189
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
- /package/.agents/skills/{_shared/pr-loop → session-continuity}/AGENTS.md +0 -0
- /package/{_shared/pr-loop → .agents/skills/session-continuity}/CLAUDE.md +0 -0
|
@@ -1,2024 +0,0 @@
|
|
|
1
|
-
"""Tests for the destructive-command-blocker hook."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
import tempfile
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
import pytest
|
|
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"
|
|
21
|
-
GH_GATE_USER_FACING_PREFIX = "[gh-gate]"
|
|
22
|
-
GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
|
|
23
|
-
GH_REDIRECT_ACTIVE_VALUE = "1"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _windows_style_system_temporary_root() -> str:
|
|
27
|
-
return tempfile.gettempdir().replace("/", "\\")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _posix_style_system_temporary_root() -> str:
|
|
31
|
-
return tempfile.gettempdir().replace("\\", "/")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _run_hook(
|
|
35
|
-
payload: str,
|
|
36
|
-
gh_redirect_active: bool = True,
|
|
37
|
-
) -> subprocess.CompletedProcess[str]:
|
|
38
|
-
child_environment = os.environ.copy()
|
|
39
|
-
if gh_redirect_active:
|
|
40
|
-
child_environment[GH_REDIRECT_ACTIVE_ENV_VAR] = GH_REDIRECT_ACTIVE_VALUE
|
|
41
|
-
else:
|
|
42
|
-
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
43
|
-
return subprocess.run(
|
|
44
|
-
[sys.executable, str(SCRIPT_PATH)],
|
|
45
|
-
input=payload,
|
|
46
|
-
text=True,
|
|
47
|
-
capture_output=True,
|
|
48
|
-
check=False,
|
|
49
|
-
env=child_environment,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_denies_gh_issue_comment_as_redirect_duplicate_guard() -> None:
|
|
54
|
-
payload = build_bash_payload(
|
|
55
|
-
'gh issue comment 83 --repo jl-cmd/claude-dev-env --body "hello"'
|
|
56
|
-
)
|
|
57
|
-
result = _run_hook(payload)
|
|
58
|
-
response = json.loads(result.stdout)
|
|
59
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
60
|
-
assert (
|
|
61
|
-
"gh issue comment" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
62
|
-
)
|
|
63
|
-
assert (
|
|
64
|
-
"duplicate execution"
|
|
65
|
-
in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def test_denies_gh_pr_comment_as_redirect_duplicate_guard() -> None:
|
|
70
|
-
payload = build_bash_payload('gh pr comment 42 --body "ok"')
|
|
71
|
-
result = _run_hook(payload)
|
|
72
|
-
response = json.loads(result.stdout)
|
|
73
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
74
|
-
assert "gh pr comment" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def test_denies_gh_pr_review_as_redirect_duplicate_guard() -> None:
|
|
78
|
-
payload = build_bash_payload("gh pr review 42 --approve")
|
|
79
|
-
result = _run_hook(payload)
|
|
80
|
-
response = json.loads(result.stdout)
|
|
81
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
82
|
-
assert "gh pr review" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def test_denies_gh_api_post_comment_as_redirect_duplicate_guard() -> None:
|
|
86
|
-
payload = build_bash_payload(
|
|
87
|
-
"gh api /repos/owner/name/issues/1/comments -X POST -f body=hello"
|
|
88
|
-
)
|
|
89
|
-
result = _run_hook(payload)
|
|
90
|
-
response = json.loads(result.stdout)
|
|
91
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_suppresses_output_on_gh_redirect_deny() -> None:
|
|
95
|
-
payload = build_bash_payload('gh issue comment 1 --body "x"')
|
|
96
|
-
result = _run_hook(payload)
|
|
97
|
-
response = json.loads(result.stdout)
|
|
98
|
-
assert response["suppressOutput"] is True
|
|
99
|
-
assert response["systemMessage"].startswith(GH_GATE_USER_FACING_PREFIX)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def test_asks_on_rm_rf_still_works() -> None:
|
|
103
|
-
payload = build_bash_payload("rm -rf /var/log/myapp")
|
|
104
|
-
result = _run_hook(payload)
|
|
105
|
-
response = json.loads(result.stdout)
|
|
106
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
107
|
-
assert "rm -rf" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def test_asks_on_git_push_force_still_works() -> None:
|
|
111
|
-
payload = build_bash_payload("git push --force origin main")
|
|
112
|
-
result = _run_hook(payload)
|
|
113
|
-
response = json.loads(result.stdout)
|
|
114
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
115
|
-
assert (
|
|
116
|
-
"git push --force" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def test_allows_plain_command_without_destructive_pattern() -> None:
|
|
121
|
-
payload = build_bash_payload("ls -la")
|
|
122
|
-
result = _run_hook(payload)
|
|
123
|
-
assert result.stdout.strip() == ""
|
|
124
|
-
assert result.returncode == 0
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def test_ignores_non_bash_tool() -> None:
|
|
128
|
-
payload = json.dumps(
|
|
129
|
-
{"tool_name": "Read", "tool_input": {"file_path": "/tmp/x"}}
|
|
130
|
-
)
|
|
131
|
-
result = _run_hook(payload)
|
|
132
|
-
assert result.stdout.strip() == ""
|
|
133
|
-
assert result.returncode == 0
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def test_gh_issue_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
137
|
-
payload = build_bash_payload('gh issue comment 83 --body "hello"')
|
|
138
|
-
|
|
139
|
-
result = _run_hook(payload, gh_redirect_active=False)
|
|
140
|
-
|
|
141
|
-
assert result.stdout.strip() == ""
|
|
142
|
-
assert result.returncode == 0
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def test_gh_pr_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
146
|
-
payload = build_bash_payload('gh pr comment 42 --body "ok"')
|
|
147
|
-
|
|
148
|
-
result = _run_hook(payload, gh_redirect_active=False)
|
|
149
|
-
|
|
150
|
-
assert result.stdout.strip() == ""
|
|
151
|
-
assert result.returncode == 0
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def test_gh_pr_review_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
155
|
-
payload = build_bash_payload("gh pr review 42 --approve")
|
|
156
|
-
|
|
157
|
-
result = _run_hook(payload, gh_redirect_active=False)
|
|
158
|
-
|
|
159
|
-
assert result.stdout.strip() == ""
|
|
160
|
-
assert result.returncode == 0
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def test_gh_api_post_comment_is_allowed_when_redirect_env_var_is_unset() -> None:
|
|
164
|
-
payload = build_bash_payload(
|
|
165
|
-
"gh api /repos/owner/name/issues/1/comments -X POST -f body=hello"
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
result = _run_hook(payload, gh_redirect_active=False)
|
|
169
|
-
|
|
170
|
-
assert result.stdout.strip() == ""
|
|
171
|
-
assert result.returncode == 0
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _run_rm_hook(payload: str) -> subprocess.CompletedProcess[str]:
|
|
175
|
-
child_environment = os.environ.copy()
|
|
176
|
-
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
177
|
-
child_environment.pop("CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW", None)
|
|
178
|
-
return subprocess.run(
|
|
179
|
-
[sys.executable, str(SCRIPT_PATH)],
|
|
180
|
-
input=payload,
|
|
181
|
-
text=True,
|
|
182
|
-
capture_output=True,
|
|
183
|
-
check=False,
|
|
184
|
-
env=child_environment,
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def test_rm_rf_asks_when_target_is_non_ephemeral_path() -> None:
|
|
189
|
-
payload = build_bash_payload("rm -rf /var/log/myapp")
|
|
190
|
-
|
|
191
|
-
result = _run_rm_hook(payload)
|
|
192
|
-
|
|
193
|
-
response = json.loads(result.stdout)
|
|
194
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
def test_rm_rf_allowed_when_target_is_under_tmp_segment() -> None:
|
|
198
|
-
payload = build_bash_payload("rm -rf /tmp/some_scratch_dir")
|
|
199
|
-
|
|
200
|
-
result = _run_rm_hook(payload)
|
|
201
|
-
|
|
202
|
-
assert result.stdout.strip() == ""
|
|
203
|
-
assert result.returncode == 0
|
|
204
|
-
|
|
205
|
-
|
|
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()
|
|
213
|
-
forward_slash_temp_path = str(system_temp_subdirectory).replace("\\", "/")
|
|
214
|
-
payload = build_bash_payload(f"rm -rf {forward_slash_temp_path}")
|
|
215
|
-
|
|
216
|
-
result = _run_rm_hook(payload)
|
|
217
|
-
|
|
218
|
-
assert result.stdout.strip() == ""
|
|
219
|
-
assert result.returncode == 0
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def test_rm_rf_allowed_when_target_is_under_worktrees_segment() -> None:
|
|
223
|
-
payload = build_bash_payload("rm -rf /Users/me/repo/worktrees/feature_branch/build")
|
|
224
|
-
|
|
225
|
-
result = _run_rm_hook(payload)
|
|
226
|
-
|
|
227
|
-
assert result.stdout.strip() == ""
|
|
228
|
-
assert result.returncode == 0
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def test_rm_rf_asks_when_target_is_bare_worktrees_directory() -> None:
|
|
232
|
-
payload = build_bash_payload("rm -rf /Users/me/repo/worktrees")
|
|
233
|
-
|
|
234
|
-
result = _run_rm_hook(payload)
|
|
235
|
-
|
|
236
|
-
response = json.loads(result.stdout)
|
|
237
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def test_rm_rf_asks_when_rm_includes_option_with_equals_sign() -> None:
|
|
241
|
-
payload = build_bash_payload("rm -rf --files0-from=/tmp/list /tmp/scratch")
|
|
242
|
-
|
|
243
|
-
result = _run_rm_hook(payload)
|
|
244
|
-
|
|
245
|
-
response = json.loads(result.stdout)
|
|
246
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def test_rm_rf_allowed_when_both_targets_are_ephemeral() -> None:
|
|
250
|
-
payload = build_bash_payload("rm -rf /tmp/first_dir /tmp/second_dir")
|
|
251
|
-
|
|
252
|
-
result = _run_rm_hook(payload)
|
|
253
|
-
|
|
254
|
-
assert result.stdout.strip() == ""
|
|
255
|
-
assert result.returncode == 0
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def test_rm_rf_asks_when_any_target_is_non_ephemeral() -> None:
|
|
259
|
-
payload = build_bash_payload("rm -rf /tmp/scratch /etc/passwd")
|
|
260
|
-
|
|
261
|
-
result = _run_rm_hook(payload)
|
|
262
|
-
|
|
263
|
-
response = json.loads(result.stdout)
|
|
264
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def test_rm_rf_asks_when_target_has_nested_temp_segment_not_at_root() -> None:
|
|
268
|
-
_assert_hook_asks("rm -rf /home/victim/temp/secret")
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
def test_rm_rf_asks_when_double_dash_includes_hyphen_prefixed_non_ephemeral_target() -> None:
|
|
272
|
-
payload = build_bash_payload("rm -rf -- /tmp/scratch -non_ephemeral")
|
|
273
|
-
|
|
274
|
-
result = _run_rm_hook(payload)
|
|
275
|
-
|
|
276
|
-
response = json.loads(result.stdout)
|
|
277
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def test_rm_rf_allowed_when_compound_with_ampersand_and_absolute_ephemeral_target() -> None:
|
|
281
|
-
payload = build_bash_payload("rm -rf /tmp/reply && gh pr checks 19")
|
|
282
|
-
|
|
283
|
-
result = _run_rm_hook(payload)
|
|
284
|
-
|
|
285
|
-
assert result.stdout.strip() == ""
|
|
286
|
-
assert result.returncode == 0
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_double_quoted() -> None:
|
|
290
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf .bugteam-tmp')
|
|
291
|
-
|
|
292
|
-
result = _run_rm_hook(payload)
|
|
293
|
-
|
|
294
|
-
assert result.stdout.strip() == ""
|
|
295
|
-
assert result.returncode == 0
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_single_quoted() -> None:
|
|
299
|
-
payload = build_bash_payload("cd '/tmp/bugteam_scratch' && rm -rf .bugteam-tmp")
|
|
300
|
-
|
|
301
|
-
result = _run_rm_hook(payload)
|
|
302
|
-
|
|
303
|
-
assert result.stdout.strip() == ""
|
|
304
|
-
assert result.returncode == 0
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_subdirectory_unquoted() -> None:
|
|
308
|
-
payload = build_bash_payload("cd /tmp/bugteam_scratch && rm -rf .bugteam-tmp")
|
|
309
|
-
|
|
310
|
-
result = _run_rm_hook(payload)
|
|
311
|
-
|
|
312
|
-
assert result.stdout.strip() == ""
|
|
313
|
-
assert result.returncode == 0
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
def test_rm_rf_allowed_when_leading_cd_into_windows_temp_worktree_subdirectory() -> None:
|
|
317
|
-
windows_style_temp_worktree = (
|
|
318
|
-
_windows_style_system_temporary_root()
|
|
319
|
-
+ r"\bugteam-pr-58-20260424071040\pr-58\worktree"
|
|
320
|
-
)
|
|
321
|
-
payload = build_bash_payload(
|
|
322
|
-
f'cd "{windows_style_temp_worktree}" && rm -rf .bugteam-tmp'
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
result = _run_rm_hook(payload)
|
|
326
|
-
|
|
327
|
-
assert result.stdout.strip() == ""
|
|
328
|
-
assert result.returncode == 0
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def test_rm_rf_allowed_when_leading_cd_into_ephemeral_with_extra_compound_after_rm() -> None:
|
|
332
|
-
payload = build_bash_payload(
|
|
333
|
-
'cd "/tmp/bugteam_scratch" && rm -rf .bugteam-tmp && gh pr checks 19'
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
result = _run_rm_hook(payload)
|
|
337
|
-
|
|
338
|
-
assert result.stdout.strip() == ""
|
|
339
|
-
assert result.returncode == 0
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def test_rm_rf_asks_when_leading_cd_into_ephemeral_with_wildcard_target() -> None:
|
|
343
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf *')
|
|
344
|
-
|
|
345
|
-
result = _run_rm_hook(payload)
|
|
346
|
-
|
|
347
|
-
response = json.loads(result.stdout)
|
|
348
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def test_rm_rf_asks_when_cwd_ephemeral_and_relative_target_escapes_via_dotdot() -> None:
|
|
352
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf ../../etc')
|
|
353
|
-
|
|
354
|
-
result = _run_rm_hook(payload)
|
|
355
|
-
|
|
356
|
-
response = json.loads(result.stdout)
|
|
357
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
def test_rm_rf_asks_when_rm_uses_files0_from_long_option_with_equals() -> None:
|
|
361
|
-
payload = build_bash_payload(
|
|
362
|
-
'cd "/tmp/bugteam_scratch" && rm -rf --files0-from=/etc/passwd'
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
result = _run_rm_hook(payload)
|
|
366
|
-
|
|
367
|
-
response = json.loads(result.stdout)
|
|
368
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
def test_rm_rf_asks_when_rm_uses_unknown_long_option() -> None:
|
|
372
|
-
payload = build_bash_payload(
|
|
373
|
-
'cd "/tmp/bugteam_scratch" && rm -rf --nuke /tmp/bugteam_scratch/stuff'
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
result = _run_rm_hook(payload)
|
|
377
|
-
|
|
378
|
-
response = json.loads(result.stdout)
|
|
379
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def test_rm_rf_asks_when_rm_target_uses_windows_backslash_absolute_path_unquoted() -> None:
|
|
383
|
-
payload = build_bash_payload(
|
|
384
|
-
r'cd "/tmp/bugteam_scratch" && rm -rf C:\sensitive\path'
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
result = _run_rm_hook(payload)
|
|
388
|
-
|
|
389
|
-
response = json.loads(result.stdout)
|
|
390
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
def test_rm_rf_asks_when_relative_target_without_declared_cwd_fails_closed() -> None:
|
|
394
|
-
payload_with_no_cwd = build_bash_payload("rm -rf relative/path")
|
|
395
|
-
|
|
396
|
-
result = _run_rm_hook(payload_with_no_cwd)
|
|
397
|
-
|
|
398
|
-
response = json.loads(result.stdout)
|
|
399
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
def test_rm_rf_allowed_when_cwd_ephemeral_and_relative_target_stays_within() -> None:
|
|
403
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf ./build')
|
|
404
|
-
|
|
405
|
-
result = _run_rm_hook(payload)
|
|
406
|
-
|
|
407
|
-
assert result.stdout.strip() == ""
|
|
408
|
-
assert result.returncode == 0
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def test_rm_rf_asks_when_tool_input_cwd_is_ephemeral_but_rm_target_is_absolute_non_ephemeral() -> None:
|
|
412
|
-
payload_with_tool_input_cwd = build_bash_payload(
|
|
413
|
-
"rm -rf /var/log/myapp", tool_input_cwd="/tmp/bugteam_scratch"
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
417
|
-
|
|
418
|
-
response = json.loads(result.stdout)
|
|
419
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
def test_rm_rf_asks_when_subshell_cd_changes_dir_before_relative_rm() -> None:
|
|
423
|
-
_assert_hook_asks('cd "/tmp/scratch" && (cd /; rm -rf etc)')
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
def test_rm_rf_asks_when_second_top_level_cd_changes_dir_before_relative_rm() -> None:
|
|
427
|
-
_assert_hook_asks('cd "/tmp/scratch" && cd / && rm -rf etc')
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def test_rm_rf_asks_when_pushd_changes_dir_before_relative_rm() -> None:
|
|
431
|
-
_assert_hook_asks('cd "/tmp/scratch" && pushd / && rm -rf etc')
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def test_rm_rf_allowed_when_subshell_cd_present_but_rm_target_is_absolute_ephemeral() -> None:
|
|
435
|
-
_assert_hook_allows('cd "/tmp/scratch" && (cd /; rm -rf /tmp/scratch/keep)')
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_target_has_nested_tmp_segment_not_at_root() -> None:
|
|
439
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf /home/victim/tmp/secret')
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def test_git_push_force_asks_when_leading_cd_into_ephemeral_subdirectory() -> None:
|
|
443
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && git push --force')
|
|
444
|
-
|
|
445
|
-
result = _run_rm_hook(payload)
|
|
446
|
-
|
|
447
|
-
response = json.loads(result.stdout)
|
|
448
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
449
|
-
assert "git push --force" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
def test_git_clean_force_recursive_asks_when_leading_cd_into_ephemeral_subdirectory() -> None:
|
|
453
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && git clean -fd')
|
|
454
|
-
|
|
455
|
-
result = _run_rm_hook(payload)
|
|
456
|
-
|
|
457
|
-
response = json.loads(result.stdout)
|
|
458
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
459
|
-
assert "git clean -fd" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
def test_rm_rf_plus_git_push_force_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
463
|
-
payload = build_bash_payload(
|
|
464
|
-
'cd "/tmp/bugteam_scratch" && rm -rf cache && git push --force'
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
result = _run_rm_hook(payload)
|
|
468
|
-
|
|
469
|
-
response = json.loads(result.stdout)
|
|
470
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def test_rm_rf_plus_git_clean_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
474
|
-
payload = build_bash_payload(
|
|
475
|
-
'cd "/tmp/bugteam_scratch" && rm -rf cache && git clean -fd'
|
|
476
|
-
)
|
|
477
|
-
|
|
478
|
-
result = _run_rm_hook(payload)
|
|
479
|
-
|
|
480
|
-
response = json.loads(result.stdout)
|
|
481
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
def test_rm_rf_plus_mkfs_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
485
|
-
payload = build_bash_payload(
|
|
486
|
-
'cd "/tmp/bugteam_scratch" && rm -rf cache && mkfs.ext4 /dev/sda1'
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
result = _run_rm_hook(payload)
|
|
490
|
-
|
|
491
|
-
response = json.loads(result.stdout)
|
|
492
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
def test_rm_rf_plus_drop_table_piggyback_asks_when_leading_cd_into_ephemeral() -> None:
|
|
496
|
-
payload = build_bash_payload(
|
|
497
|
-
'cd "/tmp/bugteam_scratch" && rm -rf cache && psql -c "DROP TABLE users"'
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
result = _run_rm_hook(payload)
|
|
501
|
-
|
|
502
|
-
response = json.loads(result.stdout)
|
|
503
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_tmp_root() -> None:
|
|
507
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf /tmp')
|
|
508
|
-
|
|
509
|
-
result = _run_rm_hook(payload)
|
|
510
|
-
|
|
511
|
-
response = json.loads(result.stdout)
|
|
512
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def test_rm_rf_asks_when_leading_cd_into_ephemeral_but_rm_target_is_bare_worktrees_root() -> None:
|
|
516
|
-
payload = build_bash_payload('cd "/tmp/bugteam_scratch" && rm -rf /worktrees')
|
|
517
|
-
|
|
518
|
-
result = _run_rm_hook(payload)
|
|
519
|
-
|
|
520
|
-
response = json.loads(result.stdout)
|
|
521
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
def test_rm_rf_asks_when_command_fails_shlex_parse_with_unbalanced_quotes() -> None:
|
|
525
|
-
payload_with_tool_input_cwd = build_bash_payload(
|
|
526
|
-
'rm -rf "unclosed_quote', tool_input_cwd="/tmp/bugteam_scratch"
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
530
|
-
|
|
531
|
-
response = json.loads(result.stdout)
|
|
532
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def test_rm_rf_asks_when_leading_cd_target_is_non_ephemeral_directory() -> None:
|
|
536
|
-
payload = build_bash_payload('cd "/etc" && rm -rf scratch')
|
|
537
|
-
|
|
538
|
-
result = _run_rm_hook(payload)
|
|
539
|
-
|
|
540
|
-
response = json.loads(result.stdout)
|
|
541
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
def test_rm_rf_asks_when_leading_cd_target_is_bare_ephemeral_root() -> None:
|
|
545
|
-
payload = build_bash_payload('cd "/tmp" && rm -rf scratch')
|
|
546
|
-
|
|
547
|
-
result = _run_rm_hook(payload)
|
|
548
|
-
|
|
549
|
-
response = json.loads(result.stdout)
|
|
550
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
def test_rm_rf_allowed_when_leading_cd_target_is_git_worktrees_directory() -> None:
|
|
554
|
-
payload = build_bash_payload('cd "/Users/me/repo/worktrees" && rm -rf feature')
|
|
555
|
-
|
|
556
|
-
result = _run_rm_hook(payload)
|
|
557
|
-
|
|
558
|
-
assert result.stdout.strip() == ""
|
|
559
|
-
assert result.returncode == 0
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
def test_rm_rf_asks_when_leading_cd_target_is_relative_path() -> None:
|
|
563
|
-
payload = build_bash_payload('cd "./scratch" && rm -rf inner')
|
|
564
|
-
|
|
565
|
-
result = _run_rm_hook(payload)
|
|
566
|
-
|
|
567
|
-
response = json.loads(result.stdout)
|
|
568
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
def test_rm_rf_allowed_for_chat_observed_bugteam_backslash_worktree_scratch_cleanup() -> None:
|
|
572
|
-
payload = build_bash_payload(
|
|
573
|
-
'cd "'
|
|
574
|
-
+ _windows_style_system_temporary_root()
|
|
575
|
-
+ r'\bugteam-pr-58-20260424071040\pr-58\worktree" && rm -rf .bugteam-tmp'
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
result = _run_rm_hook(payload)
|
|
579
|
-
|
|
580
|
-
assert result.stdout.strip() == ""
|
|
581
|
-
assert result.returncode == 0
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
def test_rm_rf_allowed_for_chat_observed_bugteam_forward_slash_worktree_scratch_cleanup() -> None:
|
|
585
|
-
payload = build_bash_payload(
|
|
586
|
-
'cd "'
|
|
587
|
-
+ _posix_style_system_temporary_root()
|
|
588
|
-
+ '/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf .bugteam-tmp'
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
result = _run_rm_hook(payload)
|
|
592
|
-
|
|
593
|
-
assert result.stdout.strip() == ""
|
|
594
|
-
assert result.returncode == 0
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
def test_rm_rf_allowed_for_chat_observed_bugfix_reply_scratch_file_cleanup() -> None:
|
|
598
|
-
payload = build_bash_payload(
|
|
599
|
-
'cd "'
|
|
600
|
-
+ _posix_style_system_temporary_root()
|
|
601
|
-
+ '/bugteam-pr-58-20260424071040/pr-58/worktree" && rm -rf tmp_reply_loop1-1.md'
|
|
602
|
-
)
|
|
603
|
-
|
|
604
|
-
result = _run_rm_hook(payload)
|
|
605
|
-
|
|
606
|
-
assert result.stdout.strip() == ""
|
|
607
|
-
assert result.returncode == 0
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
def test_rm_rf_allowed_for_chat_observed_bugfind_multiple_scratch_files_cleanup() -> None:
|
|
611
|
-
payload = build_bash_payload(
|
|
612
|
-
'cd "'
|
|
613
|
-
+ _posix_style_system_temporary_root()
|
|
614
|
-
+ '/bugteam-pr-58-20260424071040/pr-256/worktree" && rm -rf tmp_review_body.md tmp_finding_1.md'
|
|
615
|
-
)
|
|
616
|
-
|
|
617
|
-
result = _run_rm_hook(payload)
|
|
618
|
-
|
|
619
|
-
assert result.stdout.strip() == ""
|
|
620
|
-
assert result.returncode == 0
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
def test_rm_rf_allowed_via_tool_input_cwd_pointing_at_chat_observed_bugteam_worktree() -> None:
|
|
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
|
-
)
|
|
631
|
-
|
|
632
|
-
result = _run_rm_hook(payload_with_tool_input_cwd)
|
|
633
|
-
|
|
634
|
-
assert result.stdout.strip() == ""
|
|
635
|
-
assert result.returncode == 0
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
def test_rm_rf_allowed_for_chat_observed_absolute_path_in_bugteam_windows_worktree_scratch() -> None:
|
|
639
|
-
payload = build_bash_payload(
|
|
640
|
-
'rm -rf "'
|
|
641
|
-
+ _posix_style_system_temporary_root()
|
|
642
|
-
+ '/bugteam-pr-58-20260424071040/pr-58/worktree/.bugteam-tmp"'
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
result = _run_rm_hook(payload)
|
|
646
|
-
|
|
647
|
-
assert result.stdout.strip() == ""
|
|
648
|
-
assert result.returncode == 0
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
def test_rm_rf_asks_when_leading_cd_target_contains_command_substitution_dollar_parenthesis() -> None:
|
|
652
|
-
payload = build_bash_payload(
|
|
653
|
-
'cd "/tmp/$(rm -rf ~/.ssh)" && ls'
|
|
654
|
-
)
|
|
655
|
-
|
|
656
|
-
result = _run_rm_hook(payload)
|
|
657
|
-
|
|
658
|
-
response = json.loads(result.stdout)
|
|
659
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
def test_rm_rf_asks_when_leading_cd_target_contains_backtick_command_substitution() -> None:
|
|
663
|
-
payload = build_bash_payload(
|
|
664
|
-
'cd "/tmp/`rm -rf ~/.ssh`" && rm -rf .bugteam-tmp'
|
|
665
|
-
)
|
|
666
|
-
|
|
667
|
-
result = _run_rm_hook(payload)
|
|
668
|
-
|
|
669
|
-
response = json.loads(result.stdout)
|
|
670
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
def test_rm_rf_asks_when_leading_cd_target_contains_variable_expansion() -> None:
|
|
674
|
-
payload = build_bash_payload(
|
|
675
|
-
'cd "/tmp/$SNEAKY_VAR" && rm -rf .bugteam-tmp'
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
result = _run_rm_hook(payload)
|
|
679
|
-
|
|
680
|
-
response = json.loads(result.stdout)
|
|
681
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_resolve_outside_ephemeral() -> None:
|
|
685
|
-
payload = build_bash_payload(
|
|
686
|
-
'cd "/tmp/a""/../../etc" && rm -rf .'
|
|
687
|
-
)
|
|
688
|
-
|
|
689
|
-
result = _run_rm_hook(payload)
|
|
690
|
-
|
|
691
|
-
response = json.loads(result.stdout)
|
|
692
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
def test_rm_rf_asks_when_leading_cd_adjacent_quoted_strings_use_mixed_quotes() -> None:
|
|
696
|
-
payload = build_bash_payload(
|
|
697
|
-
"""cd "/tmp/a"'/../../etc' && rm -rf ."""
|
|
698
|
-
)
|
|
699
|
-
|
|
700
|
-
result = _run_rm_hook(payload)
|
|
701
|
-
|
|
702
|
-
response = json.loads(result.stdout)
|
|
703
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
def test_rm_rf_asks_when_target_is_bare_tmp_root() -> None:
|
|
707
|
-
payload = build_bash_payload("rm -rf /tmp")
|
|
708
|
-
|
|
709
|
-
result = _run_rm_hook(payload)
|
|
710
|
-
|
|
711
|
-
response = json.loads(result.stdout)
|
|
712
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
def test_rm_rf_asks_when_target_is_double_slash_tmp_root() -> None:
|
|
716
|
-
payload = build_bash_payload("rm -rf //tmp")
|
|
717
|
-
|
|
718
|
-
result = _run_rm_hook(payload)
|
|
719
|
-
|
|
720
|
-
response = json.loads(result.stdout)
|
|
721
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
722
|
-
|
|
723
|
-
|
|
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}")
|
|
731
|
-
|
|
732
|
-
result = _run_rm_hook(payload)
|
|
733
|
-
|
|
734
|
-
response = json.loads(result.stdout)
|
|
735
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
def test_rm_rf_asks_when_ephemeral_auto_allow_disabled_via_env_var() -> None:
|
|
739
|
-
payload = build_bash_payload("rm -rf /tmp/scratch")
|
|
740
|
-
|
|
741
|
-
child_environment = os.environ.copy()
|
|
742
|
-
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
743
|
-
child_environment["CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW"] = "1"
|
|
744
|
-
result = subprocess.run(
|
|
745
|
-
[sys.executable, str(SCRIPT_PATH)],
|
|
746
|
-
input=payload,
|
|
747
|
-
text=True,
|
|
748
|
-
capture_output=True,
|
|
749
|
-
check=False,
|
|
750
|
-
env=child_environment,
|
|
751
|
-
)
|
|
752
|
-
|
|
753
|
-
response = json.loads(result.stdout)
|
|
754
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
def test_rm_recursive_force_long_flags_allowed_under_tmp() -> None:
|
|
758
|
-
payload = build_bash_payload("rm --recursive --force /tmp/long_flag_scratch")
|
|
759
|
-
|
|
760
|
-
result = _run_rm_hook(payload)
|
|
761
|
-
|
|
762
|
-
assert result.stdout.strip() == ""
|
|
763
|
-
assert result.returncode == 0
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
def test_rm_rf_asks_when_quoted_dotdot_traverses_out_of_ephemeral_root() -> None:
|
|
767
|
-
payload = build_bash_payload('rm -rf /tmp/".."/etc')
|
|
768
|
-
|
|
769
|
-
result = _run_rm_hook(payload)
|
|
770
|
-
|
|
771
|
-
response = json.loads(result.stdout)
|
|
772
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
def test_rm_rf_allowed_when_quoted_path_is_legitimate_ephemeral() -> None:
|
|
776
|
-
payload = build_bash_payload('rm -rf "/tmp/some scratch dir"')
|
|
777
|
-
|
|
778
|
-
result = _run_rm_hook(payload)
|
|
779
|
-
|
|
780
|
-
assert result.stdout.strip() == ""
|
|
781
|
-
assert result.returncode == 0
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
def test_rm_rf_asks_when_single_quoted_dotdot_traverses_out_of_ephemeral() -> None:
|
|
785
|
-
payload = build_bash_payload("rm -rf /tmp/'..'/etc")
|
|
786
|
-
|
|
787
|
-
result = _run_rm_hook(payload)
|
|
788
|
-
|
|
789
|
-
response = json.loads(result.stdout)
|
|
790
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
def test_rm_rf_asks_when_target_is_glob_wildcard_under_tmp() -> None:
|
|
794
|
-
payload = build_bash_payload("rm -rf /tmp/*")
|
|
795
|
-
|
|
796
|
-
result = _run_rm_hook(payload)
|
|
797
|
-
|
|
798
|
-
response = json.loads(result.stdout)
|
|
799
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
def test_rm_rf_asks_when_target_is_question_mark_glob_under_tmp() -> None:
|
|
803
|
-
payload = build_bash_payload("rm -rf /tmp/?")
|
|
804
|
-
|
|
805
|
-
result = _run_rm_hook(payload)
|
|
806
|
-
|
|
807
|
-
response = json.loads(result.stdout)
|
|
808
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
def test_rm_rf_asks_when_target_is_bracket_glob_under_tmp() -> None:
|
|
812
|
-
payload = build_bash_payload("rm -rf /tmp/[abc]")
|
|
813
|
-
|
|
814
|
-
result = _run_rm_hook(payload)
|
|
815
|
-
|
|
816
|
-
response = json.loads(result.stdout)
|
|
817
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
def test_rm_rf_asks_when_target_is_worktrees_glob() -> None:
|
|
821
|
-
payload = build_bash_payload("rm -rf /worktrees/*")
|
|
822
|
-
|
|
823
|
-
result = _run_rm_hook(payload)
|
|
824
|
-
|
|
825
|
-
response = json.loads(result.stdout)
|
|
826
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
827
|
-
|
|
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)
|
|
835
|
-
forward_slash_temp_root = system_temporary_root.replace("\\", "/")
|
|
836
|
-
payload = build_bash_payload(f"rm -rf {forward_slash_temp_root}/*")
|
|
837
|
-
|
|
838
|
-
result = _run_rm_hook(payload)
|
|
839
|
-
|
|
840
|
-
response = json.loads(result.stdout)
|
|
841
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
842
|
-
|
|
843
|
-
|
|
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)
|
|
850
|
-
windows_style_target = system_temporary_root.replace("/", "\\") + "\\scratch"
|
|
851
|
-
payload = build_bash_payload(f"rm -rf {windows_style_target}")
|
|
852
|
-
|
|
853
|
-
result = _run_rm_hook(payload)
|
|
854
|
-
|
|
855
|
-
assert result.stdout.strip() == ""
|
|
856
|
-
assert result.returncode == 0
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
def test_rm_rf_allowed_when_unquoted_windows_backslash_target_contains_worktrees_segment() -> None:
|
|
860
|
-
payload = build_bash_payload(
|
|
861
|
-
r"rm -rf C:\Users\developer\project\worktrees\feature_branch\build"
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
result = _run_rm_hook(payload)
|
|
865
|
-
|
|
866
|
-
assert result.stdout.strip() == ""
|
|
867
|
-
assert result.returncode == 0
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
def test_rm_rf_allowed_when_finding_example_windows_backslash_ephemeral_target() -> None:
|
|
871
|
-
payload = build_bash_payload(
|
|
872
|
-
"rm -rf " + _windows_style_system_temporary_root() + r"\scratch"
|
|
873
|
-
)
|
|
874
|
-
|
|
875
|
-
result = _run_rm_hook(payload)
|
|
876
|
-
|
|
877
|
-
assert result.stdout.strip() == ""
|
|
878
|
-
assert result.returncode == 0
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
def test_rm_rf_asks_when_target_is_literal_tmp_star_finding_example() -> None:
|
|
882
|
-
payload = build_bash_payload("rm -rf /tmp/*")
|
|
883
|
-
|
|
884
|
-
result = _run_rm_hook(payload)
|
|
885
|
-
|
|
886
|
-
response = json.loads(result.stdout)
|
|
887
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
def test_rm_rf_asks_when_target_basename_is_wildcard_with_prefix_under_tmp() -> None:
|
|
891
|
-
payload = build_bash_payload("rm -rf /tmp/foo*")
|
|
892
|
-
|
|
893
|
-
result = _run_rm_hook(payload)
|
|
894
|
-
|
|
895
|
-
response = json.loads(result.stdout)
|
|
896
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
def _run_hook_with_fake_home(
|
|
900
|
-
payload: str,
|
|
901
|
-
fake_home: Path,
|
|
902
|
-
working_directory: Path,
|
|
903
|
-
disable_ephemeral_auto_allow: bool = True,
|
|
904
|
-
) -> subprocess.CompletedProcess[str]:
|
|
905
|
-
child_environment = os.environ.copy()
|
|
906
|
-
child_environment.pop(GH_REDIRECT_ACTIVE_ENV_VAR, None)
|
|
907
|
-
child_environment["HOME"] = str(fake_home)
|
|
908
|
-
child_environment["USERPROFILE"] = str(fake_home)
|
|
909
|
-
if disable_ephemeral_auto_allow:
|
|
910
|
-
child_environment["CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW"] = "1"
|
|
911
|
-
else:
|
|
912
|
-
child_environment.pop("CLAUDE_DESTRUCTIVE_DISABLE_EPHEMERAL_AUTO_ALLOW", None)
|
|
913
|
-
return subprocess.run(
|
|
914
|
-
[sys.executable, str(SCRIPT_PATH)],
|
|
915
|
-
input=payload,
|
|
916
|
-
text=True,
|
|
917
|
-
capture_output=True,
|
|
918
|
-
check=False,
|
|
919
|
-
env=child_environment,
|
|
920
|
-
cwd=str(working_directory),
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
def _write_settings_with_allow_list(fake_home: Path, allow_list: list[str]) -> None:
|
|
925
|
-
claude_directory = fake_home / ".claude"
|
|
926
|
-
claude_directory.mkdir(parents=True, exist_ok=True)
|
|
927
|
-
settings_payload = {"hooks": {"allowGitResetHardProjects": allow_list}}
|
|
928
|
-
(claude_directory / "settings.json").write_text(
|
|
929
|
-
json.dumps(settings_payload), encoding="utf-8"
|
|
930
|
-
)
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
def test_git_reset_hard_allowed_when_cwd_matches_settings_allow_list(tmp_path: Path) -> None:
|
|
934
|
-
fake_home = tmp_path / "home"
|
|
935
|
-
fake_home.mkdir()
|
|
936
|
-
project_directory = tmp_path / "approved_project"
|
|
937
|
-
project_directory.mkdir()
|
|
938
|
-
project_path_forward = str(project_directory).replace("\\", "/")
|
|
939
|
-
_write_settings_with_allow_list(fake_home, [project_path_forward])
|
|
940
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
941
|
-
|
|
942
|
-
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
943
|
-
|
|
944
|
-
assert result.stdout.strip() == ""
|
|
945
|
-
assert result.returncode == 0
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
def test_git_reset_hard_asks_when_cwd_not_in_settings_allow_list(tmp_path: Path) -> None:
|
|
949
|
-
fake_home = tmp_path / "home"
|
|
950
|
-
fake_home.mkdir()
|
|
951
|
-
approved_directory = tmp_path / "approved_project"
|
|
952
|
-
approved_directory.mkdir()
|
|
953
|
-
unapproved_directory = tmp_path / "unapproved_project"
|
|
954
|
-
unapproved_directory.mkdir()
|
|
955
|
-
approved_path_forward = str(approved_directory).replace("\\", "/")
|
|
956
|
-
_write_settings_with_allow_list(fake_home, [approved_path_forward])
|
|
957
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
958
|
-
|
|
959
|
-
result = _run_hook_with_fake_home(payload, fake_home, unapproved_directory)
|
|
960
|
-
|
|
961
|
-
response = json.loads(result.stdout)
|
|
962
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
963
|
-
assert "git reset --hard" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
def test_git_reset_hard_asks_when_settings_missing(tmp_path: Path) -> None:
|
|
967
|
-
fake_home = tmp_path / "home"
|
|
968
|
-
fake_home.mkdir()
|
|
969
|
-
project_directory = tmp_path / "unapproved_project"
|
|
970
|
-
project_directory.mkdir()
|
|
971
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
972
|
-
|
|
973
|
-
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
974
|
-
|
|
975
|
-
response = json.loads(result.stdout)
|
|
976
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
def test_git_reset_hard_asks_when_allow_list_is_empty(tmp_path: Path) -> None:
|
|
980
|
-
fake_home = tmp_path / "home"
|
|
981
|
-
fake_home.mkdir()
|
|
982
|
-
project_directory = tmp_path / "some_project"
|
|
983
|
-
project_directory.mkdir()
|
|
984
|
-
_write_settings_with_allow_list(fake_home, [])
|
|
985
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
986
|
-
|
|
987
|
-
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
988
|
-
|
|
989
|
-
response = json.loads(result.stdout)
|
|
990
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
def test_git_reset_hard_allowed_in_linked_git_worktree(tmp_path: Path) -> None:
|
|
994
|
-
fake_home = tmp_path / "home"
|
|
995
|
-
fake_home.mkdir()
|
|
996
|
-
main_repository = tmp_path / "main_repository"
|
|
997
|
-
main_repository.mkdir()
|
|
998
|
-
subprocess.run(["git", "init", "-q"], cwd=main_repository, check=True)
|
|
999
|
-
subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "init"], cwd=main_repository, check=True)
|
|
1000
|
-
worktree_directory = tmp_path / "worktree_copy"
|
|
1001
|
-
subprocess.run(
|
|
1002
|
-
["git", "worktree", "add", "-q", "-b", "feature", str(worktree_directory)],
|
|
1003
|
-
cwd=main_repository,
|
|
1004
|
-
check=True,
|
|
1005
|
-
)
|
|
1006
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
1007
|
-
|
|
1008
|
-
result = _run_hook_with_fake_home(payload, fake_home, worktree_directory, disable_ephemeral_auto_allow=False)
|
|
1009
|
-
|
|
1010
|
-
assert result.stdout.strip() == ""
|
|
1011
|
-
assert result.returncode == 0
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
def test_git_reset_hard_allowed_when_path_contains_worktrees_segment(tmp_path: Path) -> None:
|
|
1015
|
-
fake_home = tmp_path / "home"
|
|
1016
|
-
fake_home.mkdir()
|
|
1017
|
-
worktree_directory = tmp_path / "worktrees" / "some_feature"
|
|
1018
|
-
worktree_directory.mkdir(parents=True)
|
|
1019
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
1020
|
-
|
|
1021
|
-
result = _run_hook_with_fake_home(payload, fake_home, worktree_directory, disable_ephemeral_auto_allow=False)
|
|
1022
|
-
|
|
1023
|
-
assert result.stdout.strip() == ""
|
|
1024
|
-
assert result.returncode == 0
|
|
1025
|
-
|
|
1026
|
-
|
|
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")
|
|
1037
|
-
|
|
1038
|
-
result = _run_hook_with_fake_home(payload, fake_home, working_directory, disable_ephemeral_auto_allow=False)
|
|
1039
|
-
|
|
1040
|
-
assert result.stdout.strip() == ""
|
|
1041
|
-
assert result.returncode == 0
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
def test_git_reset_hard_asks_in_plain_directory_with_no_settings(tmp_path: Path) -> None:
|
|
1045
|
-
fake_home = tmp_path / "home"
|
|
1046
|
-
fake_home.mkdir()
|
|
1047
|
-
plain_directory = tmp_path / "regular_project"
|
|
1048
|
-
plain_directory.mkdir()
|
|
1049
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
1050
|
-
|
|
1051
|
-
result = _run_hook_with_fake_home(payload, fake_home, plain_directory)
|
|
1052
|
-
|
|
1053
|
-
response = json.loads(result.stdout)
|
|
1054
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
def test_git_reset_hard_asks_when_settings_file_is_invalid_json(tmp_path: Path) -> None:
|
|
1058
|
-
fake_home = tmp_path / "home"
|
|
1059
|
-
(fake_home / ".claude").mkdir(parents=True)
|
|
1060
|
-
(fake_home / ".claude" / "settings.json").write_text(
|
|
1061
|
-
"{not valid json", encoding="utf-8"
|
|
1062
|
-
)
|
|
1063
|
-
project_directory = tmp_path / "unapproved_project"
|
|
1064
|
-
project_directory.mkdir()
|
|
1065
|
-
payload = build_bash_payload("git reset --hard origin/main")
|
|
1066
|
-
|
|
1067
|
-
result = _run_hook_with_fake_home(payload, fake_home, project_directory)
|
|
1068
|
-
|
|
1069
|
-
response = json.loads(result.stdout)
|
|
1070
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
# --- compound ephemeral rm and quoted-mention guard tests ---
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
def _assert_hook_allows(command: str) -> None:
|
|
1077
|
-
result = _run_rm_hook(build_bash_payload(command))
|
|
1078
|
-
assert result.stdout.strip() == "", (
|
|
1079
|
-
f"Expected allow (no output) for {command!r}, got: {result.stdout!r}"
|
|
1080
|
-
)
|
|
1081
|
-
assert result.returncode == 0
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
def _assert_hook_asks(command: str, expected_reason_fragment: str | None = None) -> None:
|
|
1085
|
-
result = _run_rm_hook(build_bash_payload(command))
|
|
1086
|
-
response = json.loads(result.stdout)
|
|
1087
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask", (
|
|
1088
|
-
f"Expected ask for {command!r}, got: {response!r}"
|
|
1089
|
-
)
|
|
1090
|
-
if expected_reason_fragment is not None:
|
|
1091
|
-
assert (
|
|
1092
|
-
expected_reason_fragment
|
|
1093
|
-
in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
1094
|
-
), f"Reason must mention {expected_reason_fragment!r}, got: {response!r}"
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
def test_compound_rm_allowed_when_two_absolute_ephemeral_targets_then_echo() -> None:
|
|
1098
|
-
_assert_hook_allows("rm -rf /tmp/pr136 /tmp/difftest && echo 'cleaned'")
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
def test_compound_rm_allowed_when_subshell_paren_glued_rm_targets_absolute_ephemeral() -> None:
|
|
1102
|
-
_assert_hook_allows("rm -rf /tmp/a && (rm -rf /tmp/b)")
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
def test_compound_rm_asks_when_subshell_paren_glued_rm_targets_non_ephemeral() -> None:
|
|
1106
|
-
_assert_hook_asks("rm -rf /tmp/a && (rm -rf /etc)")
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
def test_compound_rm_allowed_when_followed_by_gh_pipeline_and_echo() -> None:
|
|
1110
|
-
_assert_hook_allows('rm -rf /tmp/reply && gh pr checks 19 2>&1 | head -5 && echo "x"')
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
def test_compound_rm_allowed_when_followed_by_gh_command() -> None:
|
|
1114
|
-
_assert_hook_allows("rm -rf /tmp/reply && gh pr checks 19")
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
def test_compound_rm_asks_when_shred_targets_non_ephemeral_path() -> None:
|
|
1118
|
-
_assert_hook_asks("rm -rf /tmp/x && shred -u /etc/passwd")
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
def test_compound_rm_asks_when_truncate_targets_non_ephemeral_path() -> None:
|
|
1122
|
-
_assert_hook_asks("rm -rf /tmp/x && truncate -s0 /etc/passwd")
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
def test_compound_rm_asks_when_find_delete_walks_root() -> None:
|
|
1126
|
-
_assert_hook_asks("rm -rf /tmp/x && find / -name secret -delete")
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
def test_compound_rm_asks_when_chmod_recursive_targets_non_ephemeral_path() -> None:
|
|
1130
|
-
_assert_hook_asks("rm -rf /tmp/x && chmod -R 000 /etc")
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
def test_compound_rm_asks_when_mv_moves_non_ephemeral_path() -> None:
|
|
1134
|
-
_assert_hook_asks("rm -rf /tmp/x && mv /home/user/important /tmp/x2")
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
def test_compound_rm_asks_when_gh_repo_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1138
|
-
_assert_hook_asks("rm -rf /tmp/x && gh repo delete jl-cmd/foo --yes")
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
def test_compound_rm_asks_when_git_checkout_discards_worktree_after_ephemeral_rm() -> None:
|
|
1142
|
-
_assert_hook_asks("rm -rf /tmp/x && git checkout -- .")
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
def test_compound_rm_asks_when_git_stash_drop_rides_alongside_ephemeral_rm() -> None:
|
|
1146
|
-
_assert_hook_asks("rm -rf /tmp/x && git stash drop")
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
def test_compound_rm_asks_when_git_branch_force_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1150
|
-
_assert_hook_asks("rm -rf /tmp/x && git branch -D main")
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
def test_compound_rm_asks_when_git_clean_force_rides_alongside_ephemeral_rm() -> None:
|
|
1154
|
-
_assert_hook_asks("rm -rf /tmp/x && git clean -fd")
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
def test_compound_rm_asks_when_git_rm_rides_alongside_ephemeral_rm() -> None:
|
|
1158
|
-
_assert_hook_asks("rm -rf /tmp/x && git rm -rf src")
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
def test_compound_rm_asks_when_benign_segment_redirects_into_non_ephemeral_file() -> None:
|
|
1162
|
-
_assert_hook_asks("rm -rf /tmp/x && cat /dev/null > /etc/important.conf")
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
def test_compound_rm_asks_when_benign_segment_appends_redirect_to_non_ephemeral_file() -> None:
|
|
1166
|
-
_assert_hook_asks("rm -rf /tmp/x && echo hi >> /etc/important.conf")
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
def test_compound_rm_allowed_when_gh_read_only_subcommand_follows_ephemeral_rm() -> None:
|
|
1170
|
-
_assert_hook_allows("rm -rf /tmp/reply && gh pr view 19")
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
def test_compound_rm_allowed_when_git_read_only_subcommand_follows_ephemeral_rm() -> None:
|
|
1174
|
-
_assert_hook_allows("rm -rf /tmp/reply && git status")
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
def test_compound_rm_asks_when_benign_segment_glues_redirect_to_non_ephemeral_file() -> None:
|
|
1178
|
-
_assert_hook_asks("rm -rf /tmp/x && echo pwned>/etc/passwd")
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
def test_compound_rm_asks_when_benign_segment_glues_append_redirect_to_non_ephemeral_file() -> None:
|
|
1182
|
-
_assert_hook_asks("rm -rf /tmp/x && echo hi>>/etc/important.conf")
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
def test_compound_rm_asks_when_benign_segment_uses_fd_prefixed_redirect_to_non_ephemeral_file() -> None:
|
|
1186
|
-
_assert_hook_asks("rm -rf /tmp/x && echo hi 1>/etc/important.conf")
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
def test_compound_rm_asks_when_benign_segment_uses_glued_combined_redirect_to_non_ephemeral_file() -> None:
|
|
1190
|
-
_assert_hook_asks("rm -rf /tmp/x && echo hi &>/etc/important.conf")
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
def test_compound_rm_asks_when_benign_segment_glues_redirect_directly_to_program() -> None:
|
|
1194
|
-
_assert_hook_asks("rm -rf /tmp/x && cat secret>/etc/important.conf")
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
def test_compound_rm_asks_when_sort_writes_output_file_to_non_ephemeral_path() -> None:
|
|
1198
|
-
_assert_hook_asks("rm -rf /tmp/x && sort -o /etc/important.conf /etc/passwd")
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
def test_compound_rm_asks_when_rm_target_glues_redirect_to_non_ephemeral_file() -> None:
|
|
1202
|
-
_assert_hook_asks("rm -rf /tmp/x>/etc/passwd")
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
def test_compound_rm_asks_when_rm_target_glues_append_redirect_to_non_ephemeral_file() -> None:
|
|
1206
|
-
_assert_hook_asks("rm -rf /tmp/x>>/etc/passwd")
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
def test_compound_rm_asks_when_second_rm_target_glues_redirect_to_non_ephemeral_file() -> None:
|
|
1210
|
-
_assert_hook_asks("rm -rf /tmp/a /tmp/b>/etc/hosts")
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_rm_segment_redirects_to_non_ephemeral_file() -> None:
|
|
1214
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf /tmp/x>/etc/passwd')
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_relative_rm_target_redirects_to_non_ephemeral_file() -> None:
|
|
1218
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf build>/etc/passwd')
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
def test_compound_rm_asks_when_git_config_sets_value_after_ephemeral_rm() -> None:
|
|
1222
|
-
_assert_hook_asks("rm -rf /tmp/x && git config --global user.name evil")
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
def test_compound_rm_asks_when_git_config_sets_value_equal_to_long_read_only_flag() -> None:
|
|
1226
|
-
_assert_hook_asks("rm -rf /tmp/x && git config core.editor --get")
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
def test_compound_rm_asks_when_git_config_sets_value_equal_to_list_read_only_flag() -> None:
|
|
1230
|
-
_assert_hook_asks("rm -rf /tmp/x && git config alias.x --list")
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
def test_compound_rm_asks_when_git_config_sets_value_equal_to_short_read_only_flag() -> None:
|
|
1234
|
-
_assert_hook_asks("rm -rf /tmp/x && git config core.pager -l")
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
def test_compound_rm_allowed_when_git_config_get_urlmatch_reads_after_ephemeral_rm() -> None:
|
|
1238
|
-
_assert_hook_allows("rm -rf /tmp/reply && git config --get-urlmatch a b")
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
def test_compound_rm_asks_when_git_remote_add_rides_alongside_ephemeral_rm() -> None:
|
|
1242
|
-
_assert_hook_asks("rm -rf /tmp/x && git remote add evil http://e")
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
def test_compound_rm_asks_when_git_remote_remove_rides_alongside_ephemeral_rm() -> None:
|
|
1246
|
-
_assert_hook_asks("rm -rf /tmp/x && git remote remove origin")
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
def test_compound_rm_asks_when_gh_api_http_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1250
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo -X DELETE")
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
def test_compound_rm_asks_when_gh_api_raw_field_implicit_post_rides_alongside_ephemeral_rm() -> None:
|
|
1254
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api repos/o/r/issues -f title=x")
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
def test_compound_rm_asks_when_gh_api_field_file_implicit_post_rides_alongside_ephemeral_rm() -> None:
|
|
1258
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api -F a=@b repos/o/r/comments")
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
def test_compound_rm_asks_when_gh_api_input_implicit_post_rides_alongside_ephemeral_rm() -> None:
|
|
1262
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api repos/o/r/x --input body.json")
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
def test_compound_rm_allowed_when_gh_api_field_with_explicit_get_follows_ephemeral_rm() -> None:
|
|
1266
|
-
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo -X GET -f a=b")
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
def test_compound_rm_asks_when_gh_api_glued_short_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1270
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo -XDELETE")
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
def test_compound_rm_asks_when_gh_api_glued_long_delete_rides_alongside_ephemeral_rm() -> None:
|
|
1274
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo --method=DELETE")
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
def test_compound_rm_asks_when_gh_api_glued_short_put_rides_alongside_ephemeral_rm() -> None:
|
|
1278
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo -XPUT")
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
def test_compound_rm_asks_when_gh_api_glued_long_patch_lowercase_rides_ephemeral_rm() -> None:
|
|
1282
|
-
_assert_hook_asks("rm -rf /tmp/x && gh api repos/foo --method=patch")
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
def test_compound_rm_allowed_when_gh_api_glued_short_get_follows_ephemeral_rm() -> None:
|
|
1286
|
-
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo -XGET")
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
def test_compound_rm_allowed_when_gh_api_glued_long_get_follows_ephemeral_rm() -> None:
|
|
1290
|
-
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo --method=GET")
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
def test_compound_rm_asks_when_gh_repo_delete_targets_read_only_verb_named_repo() -> None:
|
|
1294
|
-
_assert_hook_asks("rm -rf /tmp/x && gh repo delete status --yes")
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
def test_compound_rm_asks_when_git_stash_drop_targets_read_only_verb_named_ref() -> None:
|
|
1298
|
-
_assert_hook_asks("rm -rf /tmp/x && git stash drop status")
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
def test_compound_rm_asks_when_git_branch_force_delete_targets_read_only_verb_named_branch() -> None:
|
|
1302
|
-
_assert_hook_asks("rm -rf /tmp/x && git branch -D log")
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
def test_compound_rm_asks_when_git_checkout_discards_read_only_verb_named_path() -> None:
|
|
1306
|
-
_assert_hook_asks("rm -rf /tmp/x && git checkout -- log")
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
def test_compound_rm_asks_when_git_push_targets_read_only_verb_named_ref() -> None:
|
|
1310
|
-
_assert_hook_asks("rm -rf /tmp/x && git push origin log")
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
def test_compound_rm_allowed_when_gh_pr_view_takes_read_only_verb_named_argument() -> None:
|
|
1314
|
-
_assert_hook_allows("rm -rf /tmp/reply && gh pr view status")
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
def test_compound_rm_asks_when_git_remote_add_after_verbose_flag_rides_ephemeral_rm() -> None:
|
|
1318
|
-
_assert_hook_asks("rm -rf /tmp/x && git remote -v add evil http://attacker")
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
def test_compound_rm_asks_when_git_remote_set_url_after_verbose_flag_rides_ephemeral_rm() -> None:
|
|
1322
|
-
_assert_hook_asks("rm -rf /tmp/x && git remote -v set-url origin http://evil")
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
def test_compound_rm_asks_when_stdbuf_separate_output_value_wraps_bash_dash_c() -> None:
|
|
1326
|
-
_assert_hook_asks('stdbuf -o L bash -c "rm -rf /etc"')
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
def test_compound_rm_asks_when_stdbuf_long_output_value_wraps_bash_dash_c() -> None:
|
|
1330
|
-
_assert_hook_asks('stdbuf --output L bash -c "rm -rf /etc"')
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
def test_compound_rm_asks_when_stdbuf_separate_error_value_wraps_bash_dash_c() -> None:
|
|
1334
|
-
_assert_hook_asks('stdbuf -e 0 bash -c "rm -rf /etc"')
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
def test_compound_rm_asks_when_ionice_classdata_name_wraps_bash_dash_c() -> None:
|
|
1338
|
-
_assert_hook_asks('ionice --classdata foo bash -c "rm -rf /etc"')
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
def test_compound_rm_allowed_when_git_config_lists_after_ephemeral_rm() -> None:
|
|
1342
|
-
_assert_hook_allows("rm -rf /tmp/reply && git config --list")
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
def test_compound_rm_allowed_when_git_remote_lists_after_ephemeral_rm() -> None:
|
|
1346
|
-
_assert_hook_allows("rm -rf /tmp/reply && git remote -v")
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
def test_compound_rm_allowed_when_gh_api_get_follows_ephemeral_rm() -> None:
|
|
1350
|
-
_assert_hook_allows("rm -rf /tmp/reply && gh api repos/foo")
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
def test_quoted_mention_allowed_when_rm_appears_inside_grep_pattern() -> None:
|
|
1354
|
-
_assert_hook_allows("grep 'rm -rf foo' history.jsonl | tail -5")
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
def test_quoted_mention_allowed_when_rm_appears_inside_echo_argument() -> None:
|
|
1358
|
-
_assert_hook_allows('echo "rm -rf x"')
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
def test_quoted_mention_allowed_when_rm_appears_inside_git_commit_message() -> None:
|
|
1362
|
-
_assert_hook_allows('git commit -m "rm -rf cleanup"')
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
def test_compound_rm_asks_when_single_target_is_non_ephemeral() -> None:
|
|
1366
|
-
_assert_hook_asks("rm -rf /var/log/myapp")
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
def test_compound_rm_asks_when_second_rm_segment_targets_non_ephemeral() -> None:
|
|
1370
|
-
_assert_hook_asks("rm -rf /tmp/x && rm -rf /etc")
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
def test_compound_rm_asks_when_force_push_rides_alongside_ephemeral_rm() -> None:
|
|
1374
|
-
_assert_hook_asks("rm -rf /tmp/x && git push --force origin main")
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
def test_compound_rm_asks_when_git_reset_hard_rides_alongside_ephemeral_rm() -> None:
|
|
1378
|
-
_assert_hook_asks("rm -rf /tmp/x && git reset --hard")
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
def test_compound_rm_asks_when_command_substitution_present() -> None:
|
|
1382
|
-
_assert_hook_asks("rm -rf /tmp/x && echo $(whoami)")
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
def test_compound_rm_asks_when_backtick_substitution_present() -> None:
|
|
1386
|
-
_assert_hook_asks("rm -rf /tmp/x && echo `whoami`")
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
def test_compound_rm_asks_when_relative_target_without_declared_cwd() -> None:
|
|
1390
|
-
_assert_hook_asks("rm -rf scratch && echo done")
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
def test_compound_rm_asks_when_rm_token_is_a_variable_expansion() -> None:
|
|
1394
|
-
_assert_hook_asks("$RM -rf /etc")
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
def test_quoted_mention_asks_when_absolute_path_rm_runs_on_non_ephemeral() -> None:
|
|
1398
|
-
_assert_hook_asks("/bin/rm -rf /etc")
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
def test_quoted_mention_asks_when_sudo_rm_runs_on_non_ephemeral() -> None:
|
|
1402
|
-
_assert_hook_asks("sudo rm -rf /etc")
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
def test_quoted_mention_asks_when_backslash_rm_runs_on_non_ephemeral() -> None:
|
|
1406
|
-
_assert_hook_asks(r"\rm -rf /etc")
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
def test_quoted_mention_asks_when_real_force_push_rides_alongside_quoted_rm() -> None:
|
|
1410
|
-
_assert_hook_asks(
|
|
1411
|
-
"grep 'rm -rf' f && git push --force origin main",
|
|
1412
|
-
expected_reason_fragment="git push --force",
|
|
1413
|
-
)
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
def test_interpreter_execution_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1417
|
-
_assert_hook_asks("bash -c 'rm -rf /etc'")
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
def test_interpreter_execution_asks_when_sh_dash_c_runs_quoted_rm() -> None:
|
|
1421
|
-
_assert_hook_asks("sh -c 'rm -rf /home/user/x'")
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
def test_interpreter_execution_asks_when_eval_runs_quoted_rm() -> None:
|
|
1425
|
-
_assert_hook_asks("eval 'rm -rf /etc'")
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
def test_interpreter_execution_asks_when_ssh_runs_remote_quoted_rm() -> None:
|
|
1429
|
-
_assert_hook_asks("ssh host 'rm -rf /etc'")
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
def test_interpreter_execution_asks_when_python_dash_c_runs_quoted_rm() -> None:
|
|
1433
|
-
_assert_hook_asks("""python -c "import os; os.system('rm -rf /etc')\"""")
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
def test_interpreter_execution_asks_when_awk_system_runs_quoted_rm() -> None:
|
|
1437
|
-
_assert_hook_asks("""awk 'BEGIN{system("rm -rf /etc")}'""")
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
def test_interpreter_execution_asks_when_gawk_system_runs_quoted_rm() -> None:
|
|
1441
|
-
_assert_hook_asks("""gawk 'BEGIN{system("rm -rf /etc")}'""")
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
def test_interpreter_execution_asks_when_make_runs_quoted_rm() -> None:
|
|
1445
|
-
_assert_hook_asks("""make -f - 'rm -rf /etc'""")
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
def test_compound_rm_asks_when_awk_segment_runs_quoted_rm_after_ephemeral_rm() -> None:
|
|
1449
|
-
_assert_hook_asks("""rm -rf /tmp/x && awk 'BEGIN{system("rm -rf /etc")}'""")
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
def test_interpreter_execution_asks_when_benign_command_precedes_bash_dash_c() -> None:
|
|
1453
|
-
_assert_hook_asks("echo hi && bash -c 'rm -rf /etc'")
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
def test_interpreter_execution_asks_when_benign_command_precedes_ssh() -> None:
|
|
1457
|
-
_assert_hook_asks("ls && ssh host 'rm -rf /etc'")
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
def test_interpreter_execution_asks_when_benign_command_precedes_eval() -> None:
|
|
1461
|
-
_assert_hook_asks("true; eval 'rm -rf /etc'")
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
def test_interpreter_execution_asks_when_benign_command_precedes_python_dash_c() -> None:
|
|
1465
|
-
_assert_hook_asks(
|
|
1466
|
-
"""echo start && python -c "import os; os.system('rm -rf /etc')\""""
|
|
1467
|
-
)
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
def test_launcher_execution_asks_when_timeout_wraps_bash_dash_c() -> None:
|
|
1471
|
-
_assert_hook_asks("timeout 5 bash -c 'rm -rf /etc'")
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
def test_launcher_execution_asks_when_nohup_wraps_bash_dash_c() -> None:
|
|
1475
|
-
_assert_hook_asks("nohup bash -c 'rm -rf /etc'")
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
def test_launcher_execution_asks_when_nice_wraps_bash_dash_c() -> None:
|
|
1479
|
-
_assert_hook_asks("nice -n 10 bash -c 'rm -rf /etc'")
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
def test_launcher_execution_asks_when_stdbuf_wraps_bash_dash_c() -> None:
|
|
1483
|
-
_assert_hook_asks("stdbuf -oL bash -c 'rm -rf /etc'")
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
def test_launcher_execution_asks_when_time_wraps_bash_dash_c() -> None:
|
|
1487
|
-
_assert_hook_asks("time bash -c 'rm -rf /etc'")
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
def test_launcher_execution_asks_when_setsid_wraps_bash_dash_c() -> None:
|
|
1491
|
-
_assert_hook_asks("setsid bash -c 'rm -rf /etc'")
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
def test_launcher_execution_asks_when_ionice_wraps_bash_dash_c() -> None:
|
|
1495
|
-
_assert_hook_asks("ionice bash -c 'rm -rf /etc'")
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
def test_launcher_execution_asks_when_chrt_wraps_bash_dash_c() -> None:
|
|
1499
|
-
_assert_hook_asks("chrt 1 bash -c 'rm -rf /etc'")
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
def test_launcher_execution_asks_when_taskset_wraps_bash_dash_c() -> None:
|
|
1503
|
-
_assert_hook_asks("taskset -c 0 bash -c 'rm -rf /etc'")
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
def test_launcher_execution_asks_when_stacked_wrappers_precede_bash_dash_c() -> None:
|
|
1507
|
-
_assert_hook_asks("nice -n 5 timeout 5 bash -c 'rm -rf /etc'")
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
def test_launcher_execution_asks_when_timeout_wraps_python_dash_c() -> None:
|
|
1511
|
-
_assert_hook_asks(
|
|
1512
|
-
"""timeout 5 python -c "import os; os.system('rm -rf /etc')\""""
|
|
1513
|
-
)
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
def test_launcher_execution_asks_when_timeout_wraps_bash_after_ephemeral_rm() -> None:
|
|
1517
|
-
_assert_hook_asks("rm -rf /tmp/x && timeout 5 bash -c 'rm -rf /etc'")
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
def test_launcher_execution_allows_when_timeout_wraps_ephemeral_rm() -> None:
|
|
1521
|
-
_assert_hook_allows("timeout 5 rm -rf /tmp/scratch")
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
def test_launcher_execution_asks_when_taskset_hex_mask_wraps_bash_dash_c() -> None:
|
|
1525
|
-
_assert_hook_asks("taskset 0x1 bash -c 'rm -rf /etc'")
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
def test_launcher_execution_asks_when_taskset_hex_mask_absolute_path_wraps_bash_dash_c() -> None:
|
|
1529
|
-
_assert_hook_asks("/usr/bin/taskset 0xff bash -c 'rm -rf /etc'")
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
def test_launcher_execution_asks_when_taskset_cpu_range_wraps_bash_dash_c() -> None:
|
|
1533
|
-
_assert_hook_asks("taskset -c 0-3 bash -c 'rm -rf /etc'")
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
def test_launcher_execution_asks_when_timeout_duration_suffix_wraps_bash_dash_c() -> None:
|
|
1537
|
-
_assert_hook_asks("timeout 5s bash -c 'rm -rf /etc'")
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
def test_launcher_execution_asks_when_taskset_hex_mask_wraps_bash_after_ephemeral_rm() -> None:
|
|
1541
|
-
_assert_hook_asks("rm -rf /tmp/x && timeout 5s bash -c 'rm -rf /etc'")
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
def test_newline_separated_interpreter_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1545
|
-
_assert_hook_asks("echo safe\nbash -c 'rm -rf /etc'")
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
def test_carriage_return_separated_interpreter_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1549
|
-
_assert_hook_asks("echo hi\rbash -c 'rm -rf /etc'")
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
def test_brace_group_newline_interpreter_asks_when_bash_dash_c_runs_quoted_rm() -> None:
|
|
1553
|
-
_assert_hook_asks("{ echo hi\nbash -c 'rm -rf /etc'; }")
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
def test_launcher_execution_asks_when_timeout_separate_signal_value_wraps_bash_dash_c() -> None:
|
|
1557
|
-
_assert_hook_asks("timeout -s KILL 5 bash -c 'rm -rf /etc'")
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
def test_launcher_execution_asks_when_timeout_separate_sigkill_signal_wraps_bash_dash_c() -> None:
|
|
1561
|
-
_assert_hook_asks("timeout -s SIGKILL 5 bash -c 'rm -rf /etc'")
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
def test_launcher_execution_asks_when_timeout_long_signal_value_wraps_bash_dash_c() -> None:
|
|
1565
|
-
_assert_hook_asks("timeout --signal KILL 5 bash -c 'rm -rf /etc'")
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
def test_launcher_execution_asks_when_timeout_kill_after_value_wraps_bash_dash_c() -> None:
|
|
1569
|
-
_assert_hook_asks("timeout -k 1 5 bash -c 'rm -rf /etc'")
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
def test_launcher_execution_asks_when_timeout_separate_signal_wraps_bash_after_ephemeral_rm() -> None:
|
|
1573
|
-
_assert_hook_asks("rm -rf /tmp/x && timeout -s KILL 5 bash -c 'rm -rf /etc'")
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
def test_subshell_grouped_rm_asks_when_parenthesis_glued_to_rm() -> None:
|
|
1577
|
-
_assert_hook_asks("(rm -rf /etc)")
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
def test_brace_grouped_rm_asks_when_brace_glued_to_rm() -> None:
|
|
1581
|
-
_assert_hook_asks("{ rm -rf /etc; }")
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
def test_glued_semicolon_rm_asks_when_semicolon_prefixes_rm() -> None:
|
|
1585
|
-
_assert_hook_asks(";rm -rf /etc")
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
def test_glued_pipe_rm_asks_when_pipe_joins_echo_to_rm() -> None:
|
|
1589
|
-
_assert_hook_asks("echo|rm -rf /etc")
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
def test_subshell_grouped_rm_asks_when_benign_command_precedes_grouped_rm() -> None:
|
|
1593
|
-
_assert_hook_asks("echo hi; (rm -rf /etc)")
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
def test_string_execution_asks_when_subshell_paren_glued_to_bash_dash_c() -> None:
|
|
1597
|
-
_assert_hook_asks('cd "/tmp/scratch" && (bash -c \'rm -rf /etc\')')
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
def test_string_execution_asks_when_subshell_paren_glued_to_timeout_wrapping_bash() -> None:
|
|
1601
|
-
_assert_hook_asks('cd "/tmp/scratch" && (timeout 5 bash -c \'rm -rf /etc\')')
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_subshell_paren_glued_to_rm_targets_etc() -> None:
|
|
1605
|
-
_assert_hook_asks('cd "/tmp/scratch" && (rm -rf /etc)')
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_brace_glued_to_rm_targets_etc() -> None:
|
|
1609
|
-
_assert_hook_asks('cd "/tmp/scratch" && {rm -rf /etc;}')
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
def test_rm_rf_allowed_when_cd_ephemeral_and_subshell_paren_wraps_relative_ephemeral_target() -> None:
|
|
1613
|
-
_assert_hook_allows('cd "/tmp/scratch" && (rm -rf build)')
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
# --- convergence branch exemption unit tests ---
|
|
1617
|
-
|
|
1618
|
-
import importlib.util
|
|
1619
|
-
|
|
1620
|
-
_HOOK_DIR = Path(__file__).parent
|
|
1621
|
-
_hook_spec = importlib.util.spec_from_file_location(
|
|
1622
|
-
"destructive_command_blocker",
|
|
1623
|
-
_HOOK_DIR / "destructive_command_blocker.py",
|
|
1624
|
-
)
|
|
1625
|
-
assert _hook_spec is not None
|
|
1626
|
-
assert _hook_spec.loader is not None
|
|
1627
|
-
_hook_module = importlib.util.module_from_spec(_hook_spec)
|
|
1628
|
-
_hook_spec.loader.exec_module(_hook_module)
|
|
1629
|
-
_force_push_targets_convergence_branch = _hook_module._force_push_targets_convergence_branch
|
|
1630
|
-
_is_convergence_branch = _hook_module._is_convergence_branch
|
|
1631
|
-
_all_refspecs_are_convergence_branches = _hook_module._all_refspecs_are_convergence_branches
|
|
1632
|
-
_find_non_force_push_destructive_hazard = _hook_module._find_non_force_push_destructive_hazard
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
def test_convergence_branch_claude_prefix_allowed() -> None:
|
|
1636
|
-
assert _force_push_targets_convergence_branch(
|
|
1637
|
-
"git push --force origin claude/fix-123"
|
|
1638
|
-
)
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
def test_convergence_branch_worktree_prefix_allowed() -> None:
|
|
1642
|
-
assert _force_push_targets_convergence_branch(
|
|
1643
|
-
"git push --force origin worktree-pr-converge-418"
|
|
1644
|
-
)
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
def test_convergence_branch_pr_converge_allowed() -> None:
|
|
1648
|
-
assert _force_push_targets_convergence_branch(
|
|
1649
|
-
"git push --force origin pr-423-converge"
|
|
1650
|
-
)
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
def test_convergence_branch_f_variant_allowed() -> None:
|
|
1654
|
-
assert _force_push_targets_convergence_branch(
|
|
1655
|
-
"git push -f origin claude/fix-123"
|
|
1656
|
-
)
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
def test_convergence_branch_main_blocked() -> None:
|
|
1660
|
-
assert not _force_push_targets_convergence_branch(
|
|
1661
|
-
"git push --force origin main"
|
|
1662
|
-
)
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
def test_convergence_branch_refspec_destination_checked() -> None:
|
|
1666
|
-
assert not _force_push_targets_convergence_branch(
|
|
1667
|
-
"git push --force origin claude/fix:main"
|
|
1668
|
-
)
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
def test_convergence_branch_multi_refspec_main_blocked() -> None:
|
|
1672
|
-
assert not _force_push_targets_convergence_branch(
|
|
1673
|
-
"git push --force origin claude/fix-123 main"
|
|
1674
|
-
)
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
def test_convergence_branch_multi_refspec_all_convergence() -> None:
|
|
1678
|
-
assert _force_push_targets_convergence_branch(
|
|
1679
|
-
"git push --force origin claude/fix-123 worktree-other"
|
|
1680
|
-
)
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
def test_convergence_branch_multi_refspec_mixed_blocked() -> None:
|
|
1684
|
-
assert not _force_push_targets_convergence_branch(
|
|
1685
|
-
"git push --force origin claude/fix-123 main worktree-other"
|
|
1686
|
-
)
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
def test_convergence_branch_compound_main_piggyback_blocked() -> None:
|
|
1690
|
-
assert not _force_push_targets_convergence_branch(
|
|
1691
|
-
"git push --force origin claude/foo && git push --force origin main"
|
|
1692
|
-
)
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
def test_is_convergence_branch_claude_prefix() -> None:
|
|
1696
|
-
assert _is_convergence_branch("claude/fix-123")
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
def test_is_convergence_branch_worktree_prefix() -> None:
|
|
1700
|
-
assert _is_convergence_branch("worktree-pr-418")
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
def test_is_convergence_branch_pr_converge() -> None:
|
|
1704
|
-
assert _is_convergence_branch("pr-423-converge")
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
def test_is_convergence_branch_main_rejected() -> None:
|
|
1708
|
-
assert not _is_convergence_branch("main")
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
def test_is_convergence_branch_pr_converge_no_end_anchor() -> None:
|
|
1712
|
-
assert not _is_convergence_branch("pr-423-converge-extra")
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
def test_all_refspecs_empty_string_returns_false() -> None:
|
|
1716
|
-
assert not _all_refspecs_are_convergence_branches("")
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
def test_all_refspecs_whitespace_only_returns_false() -> None:
|
|
1720
|
-
assert not _all_refspecs_are_convergence_branches(" ")
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
def test_all_refspecs_flag_only_returns_false() -> None:
|
|
1724
|
-
assert not _all_refspecs_are_convergence_branches("--no-verify")
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
def test_all_refspecs_multiple_flags_only_returns_false() -> None:
|
|
1728
|
-
assert not _all_refspecs_are_convergence_branches("--no-verify --force")
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
def test_all_refspecs_flag_then_branch_checks_branch() -> None:
|
|
1732
|
-
assert not _all_refspecs_are_convergence_branches("--force main")
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
def test_all_refspecs_convergence_branch_with_flags() -> None:
|
|
1736
|
-
assert _all_refspecs_are_convergence_branches("--force claude/fix-123")
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
def test_force_push_convergence_with_no_verify_blocked() -> None:
|
|
1740
|
-
payload = build_bash_payload(
|
|
1741
|
-
"git push --force origin --no-verify claude/fix-123"
|
|
1742
|
-
)
|
|
1743
|
-
|
|
1744
|
-
result = _run_rm_hook(payload)
|
|
1745
|
-
|
|
1746
|
-
response = json.loads(result.stdout)
|
|
1747
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1748
|
-
assert "--no-verify" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
def test_force_push_convergence_with_no_gpg_sign_blocked() -> None:
|
|
1752
|
-
payload = build_bash_payload(
|
|
1753
|
-
"git push --force origin --no-gpg-sign claude/fix-123"
|
|
1754
|
-
)
|
|
1755
|
-
|
|
1756
|
-
result = _run_rm_hook(payload)
|
|
1757
|
-
|
|
1758
|
-
response = json.loads(result.stdout)
|
|
1759
|
-
assert response["hookSpecificOutput"]["permissionDecision"] == "ask"
|
|
1760
|
-
assert "--no-gpg-sign" in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
def test_force_push_convergence_allowed_when_quoted_rm_mention_precedes_push() -> None:
|
|
1764
|
-
_assert_hook_allows('echo "rm -rf foo" && git push --force origin claude/fix-123')
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
def test_force_push_main_asks_when_quoted_rm_mention_precedes_push() -> None:
|
|
1768
|
-
_assert_hook_asks(
|
|
1769
|
-
'echo "rm -rf foo" && git push --force origin main',
|
|
1770
|
-
expected_reason_fragment="git push --force",
|
|
1771
|
-
)
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
def test_compound_rm_asks_when_pipe_both_joins_tee_to_non_ephemeral_file() -> None:
|
|
1775
|
-
_assert_hook_asks("rm -rf /tmp/x && cat foo |& tee /etc/passwd")
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
def test_compound_rm_asks_when_glued_pipe_both_joins_tee_to_non_ephemeral_file() -> None:
|
|
1779
|
-
_assert_hook_asks("rm -rf /tmp/x && cat foo|&tee /etc/passwd")
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
def test_compound_rm_asks_when_pipe_both_joins_tee_append_to_non_ephemeral_file() -> None:
|
|
1783
|
-
_assert_hook_asks("rm -rf /tmp/x && cat foo |& tee -a /etc/passwd")
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
def test_find_non_force_push_hazard_detects_tee_after_pipe_both_operator() -> None:
|
|
1787
|
-
hazard_description = _find_non_force_push_destructive_hazard(
|
|
1788
|
-
"git push --force origin claude/x && cat foo |& tee /etc/passwd"
|
|
1789
|
-
)
|
|
1790
|
-
assert hazard_description is None or "rm" not in hazard_description
|
|
1791
|
-
assert not _hook_module.rm_compound_targets_only_absolute_ephemeral_paths(
|
|
1792
|
-
"rm -rf /tmp/x && cat foo |& tee /etc/passwd"
|
|
1793
|
-
)
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
def test_compound_rm_asks_when_glued_semicolon_hides_shred_segment() -> None:
|
|
1797
|
-
_assert_hook_asks("rm -rf /tmp/x && echo hi;shred -u /etc/passwd")
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
def test_compound_rm_asks_when_glued_and_operator_hides_rm_on_non_ephemeral() -> None:
|
|
1801
|
-
_assert_hook_asks("rm -rf /tmp/x && echo a&&rm -rf /etc")
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
def test_compound_rm_asks_when_glued_semicolon_hides_interpreter_running_rm() -> None:
|
|
1805
|
-
_assert_hook_asks("rm -rf /tmp/x && echo a;bash -c 'rm -rf /etc'")
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
def test_compound_rm_asks_when_glued_semicolon_hides_gh_repo_delete() -> None:
|
|
1809
|
-
_assert_hook_asks("rm -rf /tmp/x && echo a;gh repo delete jl-cmd/foo --yes")
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
def test_compound_rm_asks_when_glued_semicolon_hides_rm_after_gh_view() -> None:
|
|
1813
|
-
_assert_hook_asks("rm -rf /tmp/reply && gh pr view 1;rm -rf /etc")
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
def test_compound_rm_asks_when_glued_background_operator_hides_shred_segment() -> None:
|
|
1817
|
-
_assert_hook_asks("rm -rf /tmp/x && echo a&shred -u /etc/passwd")
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
def test_compound_rm_asks_when_glued_or_operator_hides_rm_on_non_ephemeral() -> None:
|
|
1821
|
-
_assert_hook_asks("rm -rf /tmp/x && echo a||rm -rf /etc")
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
def test_compound_rm_asks_when_newline_terminator_hides_shred_segment() -> None:
|
|
1825
|
-
_assert_hook_asks("rm -rf /tmp/x && echo a\nshred -u /etc/passwd")
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
def test_compound_rm_asks_when_git_fetch_force_refspec_rewrites_local_branch() -> None:
|
|
1829
|
-
_assert_hook_asks(
|
|
1830
|
-
"rm -rf /tmp/x && git fetch origin +refs/heads/main:refs/heads/main"
|
|
1831
|
-
)
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
def test_compound_rm_asks_when_git_fetch_long_force_flag_rewrites_local_branch() -> None:
|
|
1835
|
-
_assert_hook_asks(
|
|
1836
|
-
"rm -rf /tmp/x && git fetch --force origin refs/heads/main:refs/heads/main"
|
|
1837
|
-
)
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
def test_compound_rm_asks_when_git_fetch_short_force_flag_rewrites_local_branch() -> None:
|
|
1841
|
-
_assert_hook_asks(
|
|
1842
|
-
"rm -rf /tmp/x && git fetch -f origin refs/heads/main:refs/heads/main"
|
|
1843
|
-
)
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
def test_compound_rm_allowed_when_plain_git_fetch_follows_ephemeral_rm() -> None:
|
|
1847
|
-
_assert_hook_allows("rm -rf /tmp/reply && git fetch")
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
def test_compound_rm_allowed_when_git_fetch_origin_branch_follows_ephemeral_rm() -> None:
|
|
1851
|
-
_assert_hook_allows("rm -rf /tmp/reply && git fetch origin main")
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
def test_launcher_execution_asks_when_timeout_infinity_wraps_bash_dash_c() -> None:
|
|
1855
|
-
_assert_hook_asks("timeout inf bash -c 'rm -rf /etc'")
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
def test_launcher_execution_asks_when_timeout_millisecond_duration_wraps_bash_dash_c() -> None:
|
|
1859
|
-
_assert_hook_asks("timeout 100ms bash -c 'rm -rf /etc'")
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
def test_launcher_execution_asks_when_nice_then_timeout_infinity_wraps_bash_dash_c() -> None:
|
|
1863
|
-
_assert_hook_asks("nice -n 5 timeout inf bash -c 'rm -rf /etc'")
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
def test_launcher_execution_allows_when_timeout_infinity_wraps_ephemeral_rm() -> None:
|
|
1867
|
-
_assert_hook_allows("timeout inf rm -rf /tmp/scratch")
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
def test_launcher_execution_allows_when_timeout_seconds_wraps_ephemeral_rm() -> None:
|
|
1871
|
-
_assert_hook_allows("timeout 5 rm -rf /tmp/scratch")
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
def test_rm_rf_allowed_when_cd_worktree_then_temp_env_var_rm_then_mkdir_tar_compound() -> None:
|
|
1875
|
-
_assert_hook_allows(
|
|
1876
|
-
'cd "/Users/dev/proj/.git/worktrees/spindle" '
|
|
1877
|
-
'&& rm -rf "$TEMP/pr621_check" '
|
|
1878
|
-
'&& mkdir -p "$TEMP/pr621_check" '
|
|
1879
|
-
"&& git archive HEAD packages | tar -x -C \"$TEMP/pr621_check\" "
|
|
1880
|
-
'&& ls "$TEMP/pr621_check/packages" | head -40'
|
|
1881
|
-
)
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
def test_rm_rf_allowed_when_cd_worktree_then_find_exec_rm_then_pytest_compound() -> None:
|
|
1885
|
-
_assert_hook_allows(
|
|
1886
|
-
'cd "/Users/dev/proj/worktrees/os-update-system" '
|
|
1887
|
-
'&& find shared_utils/samsung_utils -name "__pycache__" -type d '
|
|
1888
|
-
"-exec rm -rf {} + 2>/dev/null"
|
|
1889
|
-
'; PYTHONPATH="/Users/dev/proj/worktrees/os-update-system" '
|
|
1890
|
-
'C:/Python313/python.exe -m pytest "tests/" -p no:cacheprovider -q 2>&1 | tail -15'
|
|
1891
|
-
)
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
def test_rm_rf_allowed_when_cd_ephemeral_and_sibling_mkdir_has_dash_p_flag() -> None:
|
|
1895
|
-
_assert_hook_allows('cd "/tmp/scratch" && rm -rf build && mkdir -p out')
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
def test_rm_rf_allowed_when_cd_ephemeral_and_rm_target_uses_temp_env_var() -> None:
|
|
1899
|
-
_assert_hook_allows('cd "/tmp/scratch" && rm -rf "$TEMP/build"')
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_bash_dash_c_executes_rm_on_non_ephemeral() -> None:
|
|
1903
|
-
_assert_hook_asks("cd \"/tmp/scratch\" && rm -rf build && bash -c 'rm -rf /etc'")
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_rm_target_uses_non_temp_env_var() -> None:
|
|
1907
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf "$HOME/important"')
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
def test_rm_rf_asks_when_cd_ephemeral_and_second_rm_segment_targets_non_ephemeral() -> None:
|
|
1911
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf build && rm -rf /etc/passwd')
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_bin_rm_targets_non_ephemeral() -> None:
|
|
1915
|
-
_assert_hook_asks('cd "/tmp/scratch" && /bin/rm -rf /etc')
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_target_is_command_substitution() -> None:
|
|
1919
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf $(somecmd)')
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_target_is_brace_expansion_escaping_namespace() -> None:
|
|
1923
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf {build,/etc}')
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_temp_var_splices_after_absolute_literal_prefix() -> None:
|
|
1927
|
-
_assert_hook_asks('cd "/tmp/scratch" && rm -rf /data$TMP/x')
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_rm_search_root_escapes_namespace() -> None:
|
|
1931
|
-
_assert_hook_asks('cd "/tmp/scratch" && find /etc -name x -exec rm -rf {} +')
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_subshell_find_exec_rm_search_root_escapes() -> None:
|
|
1935
|
-
_assert_hook_asks('cd "/tmp/scratch" && (find /etc -exec rm -rf {} +)')
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
def test_rm_rf_asks_when_find_exec_rm_safe_but_sibling_standalone_rm_targets_non_ephemeral() -> None:
|
|
1939
|
-
_assert_hook_asks(
|
|
1940
|
-
'cd "/tmp/scratch" && find . -name x -exec rm -rf {} + ; rm -rf /etc/passwd'
|
|
1941
|
-
)
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_rm_redirects_to_non_ephemeral_file() -> None:
|
|
1945
|
-
_assert_hook_asks('cd "/tmp/scratch" && find /tmp/scratch -exec rm -rf {} + >/etc/passwd')
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
def test_rm_rf_allowed_when_cd_ephemeral_and_relative_build_target() -> None:
|
|
1949
|
-
_assert_hook_allows('cd "/tmp/scratch" && rm -rf build')
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
def test_rm_rf_allowed_when_cd_ephemeral_and_find_exec_rm_search_root_is_dot() -> None:
|
|
1953
|
-
_assert_hook_allows('cd "/tmp/scratch" && find . -name x -exec rm -rf {} +')
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_bash_dash_c_deletes_non_ephemeral() -> None:
|
|
1957
|
-
_assert_hook_asks("cd \"/tmp/scratch\" && find . -exec bash -c 'rm -rf /etc' \\;")
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_sh_dash_c_deletes_non_ephemeral() -> None:
|
|
1961
|
-
_assert_hook_asks("cd \"/tmp/scratch\" && find . -exec sh -c 'rm -rf /etc' \\;")
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_find_execdir_bash_dash_c_deletes_non_ephemeral() -> None:
|
|
1965
|
-
_assert_hook_asks("cd \"/tmp/scratch\" && find . -execdir bash -c 'rm -rf /etc' \\;")
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
def test_rm_rf_asks_when_cd_ephemeral_but_find_exec_python_dash_c_deletes_non_ephemeral() -> None:
|
|
1969
|
-
_assert_hook_asks(
|
|
1970
|
-
"cd \"/tmp/scratch\" && find . -exec python -c 'import os; os.system(\"rm -rf /etc\")' \\;"
|
|
1971
|
-
)
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
# H1: find global option before the search root must not defeat the escape check
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
def test_rm_rf_asks_when_find_dash_l_global_option_precedes_non_ephemeral_search_root() -> None:
|
|
1978
|
-
_assert_hook_asks('cd "/tmp/scratch" && find -L /etc -name x -exec rm -rf {} +')
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
def test_rm_rf_asks_when_find_dash_p_global_option_precedes_non_ephemeral_execdir_root() -> None:
|
|
1982
|
-
_assert_hook_asks('cd "/tmp/scratch" && find -P /etc -execdir rm -rf {} +')
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
def test_rm_rf_asks_when_find_optimization_level_option_precedes_non_ephemeral_search_root() -> None:
|
|
1986
|
-
_assert_hook_asks('cd "/tmp/scratch" && find -O3 /etc -exec rm -rf {} +')
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
def test_rm_rf_asks_when_standalone_find_optimization_option_precedes_non_ephemeral_search_root() -> None:
|
|
1990
|
-
_assert_hook_asks('cd "/tmp/scratch" && find -O /etc -exec rm -rf {} +')
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
def test_rm_rf_asks_when_find_debug_option_value_precedes_non_ephemeral_search_root() -> None:
|
|
1994
|
-
_assert_hook_asks('cd "/tmp/scratch" && find -D tree /etc -exec rm -rf {} +')
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
def test_rm_rf_allowed_when_find_global_option_precedes_ephemeral_dot_search_root() -> None:
|
|
1998
|
-
_assert_hook_allows('cd "/tmp/scratch" && find -L . -name x -exec rm -rf {} +')
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
# H2: multi -exec with a \\; terminator must not sever the destructive action from detection
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
def test_rm_rf_asks_when_multi_exec_second_action_runs_bash_dash_c_deleting_non_ephemeral() -> None:
|
|
2005
|
-
_assert_hook_asks(
|
|
2006
|
-
"cd \"/tmp/scratch\" && find . -exec touch {} \\; -exec bash -c 'rm -rf /etc' \\;"
|
|
2007
|
-
)
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
def test_rm_rf_asks_when_multi_exec_second_action_runs_sh_dash_c_deleting_non_ephemeral() -> None:
|
|
2011
|
-
_assert_hook_asks(
|
|
2012
|
-
"cd \"/tmp/scratch\" && find . -exec echo {} \\; -exec sh -c 'rm -rf /etc' \\;"
|
|
2013
|
-
)
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
def test_rm_rf_allowed_when_multi_exec_both_actions_target_only_ephemeral_paths() -> None:
|
|
2017
|
-
_assert_hook_allows("cd \"/tmp/scratch\" && find . -exec echo {} \\; -exec rm -rf {} \\;")
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
# H3: parallel forwarding an interpreter that deletes a non-ephemeral path must ask
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
def test_rm_rf_asks_when_parallel_forwards_bash_dash_c_deleting_non_ephemeral() -> None:
|
|
2024
|
-
_assert_hook_asks("cd \"/tmp/scratch\" && parallel bash -c 'rm -rf /etc' ::: x")
|