claude-dev-env 2.4.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +26 -59
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -2
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +36 -8
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/codex-capability-map.json +13 -0
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +35 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +4 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +257 -23
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +10 -12
- package/hooks/hooks_constants/CLAUDE.md +7 -2
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +13 -3
- package/rules/CLAUDE.md +17 -22
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +53 -44
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review.py +85 -908
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +168 -1
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +72 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +177 -22
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +129 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/session-log/SKILL.md +1 -1
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/tdd.md +0 -7
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -300,6 +300,15 @@ def test_plain_language_denial_on_multi_edit_of_markdown_file() -> None:
|
|
|
300
300
|
_assert_dispatcher_matches_individual_hooks(payload_text, MULTI_EDIT_TOOL_NAME)
|
|
301
301
|
|
|
302
302
|
|
|
303
|
+
def test_dispatcher_docstring_points_at_roster_not_hardcoded_counts() -> None:
|
|
304
|
+
"""The dispatcher docstring names the roster, not per-tool counts that drift."""
|
|
305
|
+
dispatcher_source = Path(_DISPATCHER_SCRIPT).read_text(encoding="utf-8")
|
|
306
|
+
assert "ALL_HOSTED_HOOK_ENTRIES" in dispatcher_source
|
|
307
|
+
assert "-> 20 hooks" not in dispatcher_source
|
|
308
|
+
assert "-> 21 hooks" not in dispatcher_source
|
|
309
|
+
assert "-> 9 hooks" not in dispatcher_source
|
|
310
|
+
|
|
311
|
+
|
|
303
312
|
def test_multi_edit_runs_only_group_b_hooks() -> None:
|
|
304
313
|
"""Dispatcher invokes only Group-B hooks on MultiEdit, not Group-A hooks.
|
|
305
314
|
|
|
@@ -667,7 +676,7 @@ def test_proceed_after_run_all_validators_removal_allows() -> None:
|
|
|
667
676
|
it was never a PreToolUse hook and never hosted by the PreToolUse dispatcher.
|
|
668
677
|
A Python Write payload that run_all_validators would have flagged (mypy errors, for
|
|
669
678
|
instance) still produces ALLOW from the PreToolUse dispatcher because the PreToolUse
|
|
670
|
-
dispatcher covers only its
|
|
679
|
+
dispatcher covers only its 23 hosted blocking hooks — none of which includes the
|
|
671
680
|
validators runner.
|
|
672
681
|
"""
|
|
673
682
|
python_content_with_type_error = (
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Tests for shell_substitution_blocker hook."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
SCRIPT_PATH = Path(__file__).parent / "shell_substitution_blocker.py"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
|
|
13
|
+
return subprocess.run(
|
|
14
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
15
|
+
input=json.dumps(payload),
|
|
16
|
+
text=True,
|
|
17
|
+
capture_output=True,
|
|
18
|
+
check=False,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_denies_dollar_paren_substitution() -> None:
|
|
23
|
+
payload = {
|
|
24
|
+
"tool_name": "Bash",
|
|
25
|
+
"tool_input": {"command": 'echo "head: $(git rev-parse HEAD)"'},
|
|
26
|
+
}
|
|
27
|
+
response = json.loads(_run_hook(payload).stdout)
|
|
28
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
29
|
+
assert (
|
|
30
|
+
"shell-substitution"
|
|
31
|
+
in response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_denies_unescaped_backtick_substitution() -> None:
|
|
36
|
+
payload = {
|
|
37
|
+
"tool_name": "Bash",
|
|
38
|
+
"tool_input": {"command": "echo `date`"},
|
|
39
|
+
}
|
|
40
|
+
response = json.loads(_run_hook(payload).stdout)
|
|
41
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_allows_plain_command_without_substitution() -> None:
|
|
45
|
+
payload = {
|
|
46
|
+
"tool_name": "Bash",
|
|
47
|
+
"tool_input": {"command": "git rev-parse HEAD"},
|
|
48
|
+
}
|
|
49
|
+
assert _run_hook(payload).stdout == ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_allows_escaped_backtick_in_prose() -> None:
|
|
53
|
+
payload = {
|
|
54
|
+
"tool_name": "Bash",
|
|
55
|
+
"tool_input": {"command": r'echo "use \`foo\` like this"'},
|
|
56
|
+
}
|
|
57
|
+
assert _run_hook(payload).stdout == ""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_ignores_non_bash_tool() -> None:
|
|
61
|
+
payload = {
|
|
62
|
+
"tool_name": "Edit",
|
|
63
|
+
"tool_input": {"command": 'echo "$(date)"'},
|
|
64
|
+
}
|
|
65
|
+
assert _run_hook(payload).stdout == ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_denies_double_backslash_backtick_bypass() -> None:
|
|
69
|
+
payload = {
|
|
70
|
+
"tool_name": "Bash",
|
|
71
|
+
"tool_input": {"command": r"echo \\`date`"},
|
|
72
|
+
}
|
|
73
|
+
response = json.loads(_run_hook(payload).stdout)
|
|
74
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_allows_dollar_paren_inside_single_quotes() -> None:
|
|
78
|
+
payload = {
|
|
79
|
+
"tool_name": "Bash",
|
|
80
|
+
"tool_input": {"command": "echo '$(not-executed)'"},
|
|
81
|
+
}
|
|
82
|
+
assert _run_hook(payload).stdout == ""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_allows_backtick_inside_single_quotes() -> None:
|
|
86
|
+
payload = {
|
|
87
|
+
"tool_name": "Bash",
|
|
88
|
+
"tool_input": {"command": "echo '`not-executed`'"},
|
|
89
|
+
}
|
|
90
|
+
assert _run_hook(payload).stdout == ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_denies_input_process_substitution() -> None:
|
|
94
|
+
payload = {
|
|
95
|
+
"tool_name": "Bash",
|
|
96
|
+
"tool_input": {"command": "diff <(cat a) <(cat b)"},
|
|
97
|
+
}
|
|
98
|
+
response = json.loads(_run_hook(payload).stdout)
|
|
99
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_denies_output_process_substitution() -> None:
|
|
103
|
+
payload = {
|
|
104
|
+
"tool_name": "Bash",
|
|
105
|
+
"tool_input": {"command": "tee >(gzip > out.gz)"},
|
|
106
|
+
}
|
|
107
|
+
response = json.loads(_run_hook(payload).stdout)
|
|
108
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_allows_process_substitution_inside_single_quotes() -> None:
|
|
112
|
+
payload = {
|
|
113
|
+
"tool_name": "Bash",
|
|
114
|
+
"tool_input": {"command": "echo '<(not-executed)'"},
|
|
115
|
+
}
|
|
116
|
+
assert _run_hook(payload).stdout == ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_allows_arithmetic_expansion() -> None:
|
|
120
|
+
payload = {
|
|
121
|
+
"tool_name": "Bash",
|
|
122
|
+
"tool_input": {"command": "echo $((2+2))"},
|
|
123
|
+
}
|
|
124
|
+
assert _run_hook(payload).stdout == ""
|
|
@@ -173,6 +173,29 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
|
|
|
173
173
|
assert "probably" in parsed["reason"].lower() or "hedging" in parsed["reason"].lower()
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
def test_dispatcher_blocks_overlong_reply_matching_standalone() -> None:
|
|
177
|
+
"""An overlong last_assistant_message blocks through the dispatcher."""
|
|
178
|
+
overlong_message = "\n".join(
|
|
179
|
+
" ".join(f"finding{each_index}" for each_index in range(10))
|
|
180
|
+
for _ in range(30)
|
|
181
|
+
)
|
|
182
|
+
payload_text = json.dumps(
|
|
183
|
+
{"stop_hook_active": False, "last_assistant_message": overlong_message}
|
|
184
|
+
)
|
|
185
|
+
completed = subprocess.run(
|
|
186
|
+
[sys.executable, _DISPATCHER_SCRIPT],
|
|
187
|
+
check=False,
|
|
188
|
+
input=payload_text,
|
|
189
|
+
capture_output=True,
|
|
190
|
+
text=True,
|
|
191
|
+
encoding="utf-8",
|
|
192
|
+
)
|
|
193
|
+
assert completed.returncode == 0
|
|
194
|
+
parsed = json.loads(completed.stdout)
|
|
195
|
+
assert parsed["decision"] == "block"
|
|
196
|
+
assert "ELI11 REPLY SHAPE" in parsed["reason"]
|
|
197
|
+
|
|
198
|
+
|
|
176
199
|
def test_dispatcher_imports_standalone_with_only_blocking_on_the_path() -> None:
|
|
177
200
|
"""The dispatcher's bootstrap resolves hooks_constants without hooks/ on PYTHONPATH."""
|
|
178
201
|
subprocess_environment = {**os.environ, "PYTHONPATH": str(_BLOCKING_DIR)}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tests for unscoped_search_blocker hook."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SCRIPT_PATH = Path(__file__).parent / "unscoped_search_blocker.py"
|
|
12
|
+
|
|
13
|
+
ALL_DENIED_COMMANDS = [
|
|
14
|
+
"find / -iname code_rules_gate.py",
|
|
15
|
+
"find /c -name '*.py'",
|
|
16
|
+
"find ~ -name README.md",
|
|
17
|
+
"find $HOME -type f",
|
|
18
|
+
"ls -R /",
|
|
19
|
+
"ls -laR /",
|
|
20
|
+
"ls -Rl /",
|
|
21
|
+
"find //server/share -name x",
|
|
22
|
+
"find \\\\server\\share -name x",
|
|
23
|
+
"bash -c 'find / -name x'",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
ALL_ALLOWED_COMMANDS = [
|
|
27
|
+
"find . -iname '*.py'",
|
|
28
|
+
"find packages/claude-dev-env -name code_rules_gate.py",
|
|
29
|
+
"find /c/Users/example/repo -iname SKILL.md",
|
|
30
|
+
"ls -r /",
|
|
31
|
+
"ls -la /tmp/x",
|
|
32
|
+
"find //server/share/project/src -name x",
|
|
33
|
+
"es.exe path:C:\\dev\\repo ext:py gate",
|
|
34
|
+
"git status --short",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _run_hook(payload: dict) -> subprocess.CompletedProcess[str]:
|
|
39
|
+
return subprocess.run(
|
|
40
|
+
[sys.executable, str(SCRIPT_PATH)],
|
|
41
|
+
input=json.dumps(payload),
|
|
42
|
+
text=True,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
check=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _bash_payload(command: str) -> dict:
|
|
49
|
+
return {"tool_name": "Bash", "tool_input": {"command": command}}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _powershell_payload(command: str) -> dict:
|
|
53
|
+
return {"tool_name": "PowerShell", "tool_input": {"command": command}}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("each_command", ALL_DENIED_COMMANDS)
|
|
57
|
+
def test_denies_unscoped_root_walk(each_command: str) -> None:
|
|
58
|
+
response = json.loads(_run_hook(_bash_payload(each_command)).stdout)
|
|
59
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.parametrize("each_command", ALL_ALLOWED_COMMANDS)
|
|
63
|
+
def test_allows_scoped_search(each_command: str) -> None:
|
|
64
|
+
assert _run_hook(_bash_payload(each_command)).stdout == ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_denies_recursive_listing_on_windows_drive_root() -> None:
|
|
68
|
+
response = json.loads(
|
|
69
|
+
_run_hook(_powershell_payload("Get-ChildItem -Path C:\\ -Recurse")).stdout
|
|
70
|
+
)
|
|
71
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_allows_recursive_listing_under_project_directory() -> None:
|
|
75
|
+
assert _run_hook(_powershell_payload("Get-ChildItem -Path .\\src -Recurse")).stdout == ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_allows_recursive_listing_without_recurse_flag() -> None:
|
|
79
|
+
assert _run_hook(_powershell_payload("Get-ChildItem -Path C:\\")).stdout == ""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_denies_find_root_in_a_chained_segment() -> None:
|
|
83
|
+
response = json.loads(
|
|
84
|
+
_run_hook(_bash_payload("cd /tmp && find / -name x.py")).stdout
|
|
85
|
+
)
|
|
86
|
+
assert response["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_ignores_unsupported_tool_name() -> None:
|
|
90
|
+
payload = {"tool_name": "Edit", "tool_input": {"command": "find / -name x"}}
|
|
91
|
+
assert _run_hook(payload).stdout == ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_ignores_payload_without_a_command() -> None:
|
|
95
|
+
payload = {"tool_name": "Bash", "tool_input": {}}
|
|
96
|
+
assert _run_hook(payload).stdout == ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_deny_message_names_the_scoped_alternative() -> None:
|
|
100
|
+
response = json.loads(_run_hook(_bash_payload("find / -name x.py")).stdout)
|
|
101
|
+
deny_reason = response["hookSpecificOutput"]["permissionDecisionReason"]
|
|
102
|
+
assert "scope" in deny_reason.lower()
|