claude-dev-env 1.95.0 → 2.0.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/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +20 -20
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
- package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/ever-shipped-skills.mjs +3 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +5 -2
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +1 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/tdd_enforcer.py +97 -608
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +13 -36
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +5 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +200 -16
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +15 -11
- package/skills/orchestrator-refresh/SKILL.md +5 -5
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/team-advisor/SKILL.md +7 -7
|
@@ -16,7 +16,9 @@ module so the hooks.json wiring and every importer resolve unchanged.
|
|
|
16
16
|
|
|
17
17
|
A repository named in ``CLAUDE_PII_EXEMPT_REPOS`` or the ``pii_exempt_repositories``
|
|
18
18
|
list in ``~/.claude/local-identity.json`` skips the staged-commit scan; a
|
|
19
|
-
repository without a readable origin remote is never exempt.
|
|
19
|
+
repository without a readable origin remote is never exempt. Exact values listed
|
|
20
|
+
under ``pii_allowlisted_values`` for a repository's origin slug may pass the
|
|
21
|
+
Write/Edit and staged-commit scans in that repository only.
|
|
20
22
|
"""
|
|
21
23
|
|
|
22
24
|
from __future__ import annotations
|
|
@@ -25,67 +27,83 @@ import json
|
|
|
25
27
|
import subprocess
|
|
26
28
|
import sys
|
|
27
29
|
from pathlib import Path
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
31
|
-
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
from
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
from
|
|
55
|
-
from
|
|
56
|
-
from
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
33
|
+
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
34
|
+
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
35
|
+
if each_bootstrap_directory not in sys.path:
|
|
36
|
+
sys.path.insert(0, each_bootstrap_directory)
|
|
37
|
+
from pii_commit_command import (
|
|
38
|
+
extract_git_commit_working_directory,
|
|
39
|
+
is_git_commit_shell_command,
|
|
40
|
+
)
|
|
41
|
+
from pii_payload_scan import (
|
|
42
|
+
build_deny_reason,
|
|
43
|
+
evaluate_post_body_texts,
|
|
44
|
+
evaluate_write_edit_payload,
|
|
45
|
+
)
|
|
46
|
+
from pii_prevention_blocker_parts.repository_exemption import (
|
|
47
|
+
_is_repository_exempt_from_pii_scan,
|
|
48
|
+
_owner_repo_slug_from_origin_url,
|
|
49
|
+
repository_allowlisted_values,
|
|
50
|
+
)
|
|
51
|
+
from pii_prevention_blocker_parts.repository_resolution import (
|
|
52
|
+
compose_command_working_directory,
|
|
53
|
+
expand_user_directory,
|
|
54
|
+
refusal_reason_for_unresolved_repository,
|
|
55
|
+
)
|
|
56
|
+
from pii_scanner import is_path_exempt_from_pii_scan, scan_text_for_pii
|
|
57
|
+
from precommit_code_rules_gate import resolve_repository_root
|
|
58
|
+
from volatile_path_in_post_blocker import (
|
|
59
|
+
extract_gh_post_body_texts_for_privacy_gate,
|
|
60
|
+
extract_mcp_body_texts,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
from hooks_constants.hook_block_logger import log_hook_block
|
|
64
|
+
from hooks_constants.pii_prevention_constants import (
|
|
65
|
+
ALL_SHELL_TOOL_NAMES,
|
|
66
|
+
ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX,
|
|
67
|
+
ALL_STAGED_FILES_COMMAND,
|
|
68
|
+
ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
69
|
+
BODY_FILE_ENCODING,
|
|
70
|
+
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
71
|
+
HOOK_SCRIPT_BASENAME,
|
|
72
|
+
MAXIMUM_STAGED_FILE_BYTES,
|
|
73
|
+
MCP_GITHUB_TOOL_PREFIX,
|
|
74
|
+
NULL_BYTE_MARKER,
|
|
75
|
+
STAGED_BLOB_PREFIX,
|
|
76
|
+
STAGED_BLOB_REASON_DECODE_FAILED,
|
|
77
|
+
STAGED_BLOB_REASON_GIT_SHOW_FAILED,
|
|
78
|
+
STAGED_BLOB_REASON_NULL_BYTES,
|
|
79
|
+
STAGED_BLOB_REASON_OVERSIZED,
|
|
80
|
+
STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE,
|
|
81
|
+
STAGED_LIST_FAILURE_REASON,
|
|
82
|
+
)
|
|
83
|
+
from hooks_constants.pre_tool_use_stdin import (
|
|
84
|
+
read_hook_input_dictionary_from_stdin,
|
|
85
|
+
)
|
|
86
|
+
except ImportError as import_error:
|
|
87
|
+
raise ImportError(
|
|
88
|
+
"pii_prevention_blocker: cannot import its sibling modules; "
|
|
89
|
+
"ensure the blocking and hooks directories are importable."
|
|
90
|
+
) from import_error
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"evaluate",
|
|
95
|
+
"evaluate_bash_command",
|
|
96
|
+
"evaluate_post_body_texts",
|
|
97
|
+
"evaluate_staged_commit",
|
|
98
|
+
"evaluate_write_edit_payload",
|
|
99
|
+
"extract_git_commit_working_directory",
|
|
100
|
+
"is_git_commit_shell_command",
|
|
101
|
+
"list_staged_file_paths",
|
|
102
|
+
"read_staged_file_text",
|
|
103
|
+
"resolve_repository_root",
|
|
104
|
+
"_owner_repo_slug_from_origin_url",
|
|
105
|
+
"main",
|
|
106
|
+
]
|
|
89
107
|
|
|
90
108
|
|
|
91
109
|
def list_staged_file_paths(
|
|
@@ -103,7 +121,7 @@ def list_staged_file_paths(
|
|
|
103
121
|
try:
|
|
104
122
|
completed_process = subprocess.run(
|
|
105
123
|
list(ALL_STAGED_FILES_COMMAND),
|
|
106
|
-
capture_output=True,
|
|
124
|
+
check=False, capture_output=True,
|
|
107
125
|
text=True,
|
|
108
126
|
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
109
127
|
cwd=str(repository_root),
|
|
@@ -120,64 +138,85 @@ def list_staged_file_paths(
|
|
|
120
138
|
return all_paths, None
|
|
121
139
|
|
|
122
140
|
|
|
141
|
+
def _git_show_staged_blob(
|
|
142
|
+
repository_root: Path, staged_blob_reference: str
|
|
143
|
+
) -> subprocess.CompletedProcess[bytes] | None:
|
|
144
|
+
"""Return the ``git show`` result for a staged blob, or None on failure."""
|
|
145
|
+
try:
|
|
146
|
+
completed_process = subprocess.run(
|
|
147
|
+
list(ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX) + [staged_blob_reference],
|
|
148
|
+
check=False, capture_output=True,
|
|
149
|
+
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
150
|
+
cwd=str(repository_root),
|
|
151
|
+
)
|
|
152
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
153
|
+
return None
|
|
154
|
+
if completed_process.returncode != 0:
|
|
155
|
+
return None
|
|
156
|
+
return completed_process
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _unscannable_result(relative_path: str, reason: str) -> tuple[None, str]:
|
|
160
|
+
"""Build the ``(None, deny_reason)`` pair for an unscannable staged blob."""
|
|
161
|
+
return None, STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE.format(
|
|
162
|
+
relative_path=relative_path, reason=reason
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
123
166
|
def read_staged_file_text(
|
|
124
167
|
repository_root: Path, relative_path: str
|
|
125
168
|
) -> tuple[str | None, str | None]:
|
|
126
|
-
"""Return staged blob text, or
|
|
169
|
+
"""Return staged blob text, or ``(None, deny_reason)`` when unscannable.
|
|
127
170
|
|
|
128
171
|
Args:
|
|
129
172
|
repository_root: Repository root for the git show working directory.
|
|
130
173
|
relative_path: Repository-relative path of the staged file.
|
|
131
174
|
|
|
132
175
|
Returns:
|
|
133
|
-
``(text, None)``
|
|
134
|
-
``(None, deny_reason)`` when the blob cannot be scanned (fail-closed).
|
|
176
|
+
``(text, None)`` for scannable UTF-8 text; ``(None, reason)`` otherwise.
|
|
135
177
|
"""
|
|
136
178
|
staged_blob_reference = STAGED_BLOB_PREFIX + relative_path
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
capture_output=True,
|
|
141
|
-
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
142
|
-
cwd=str(repository_root),
|
|
143
|
-
)
|
|
144
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
145
|
-
return None, STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE.format(
|
|
146
|
-
relative_path=relative_path,
|
|
147
|
-
reason=STAGED_BLOB_REASON_GIT_SHOW_FAILED,
|
|
148
|
-
)
|
|
149
|
-
if completed_process.returncode != 0:
|
|
150
|
-
return None, STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE.format(
|
|
151
|
-
relative_path=relative_path,
|
|
152
|
-
reason=STAGED_BLOB_REASON_GIT_SHOW_FAILED,
|
|
153
|
-
)
|
|
179
|
+
completed_process = _git_show_staged_blob(repository_root, staged_blob_reference)
|
|
180
|
+
if completed_process is None:
|
|
181
|
+
return _unscannable_result(relative_path, STAGED_BLOB_REASON_GIT_SHOW_FAILED)
|
|
154
182
|
raw_bytes = completed_process.stdout
|
|
155
183
|
if len(raw_bytes) > MAXIMUM_STAGED_FILE_BYTES:
|
|
156
|
-
return
|
|
157
|
-
relative_path=relative_path,
|
|
158
|
-
reason=STAGED_BLOB_REASON_OVERSIZED,
|
|
159
|
-
)
|
|
184
|
+
return _unscannable_result(relative_path, STAGED_BLOB_REASON_OVERSIZED)
|
|
160
185
|
if NULL_BYTE_MARKER in raw_bytes:
|
|
161
|
-
return
|
|
162
|
-
relative_path=relative_path,
|
|
163
|
-
reason=STAGED_BLOB_REASON_NULL_BYTES,
|
|
164
|
-
)
|
|
186
|
+
return _unscannable_result(relative_path, STAGED_BLOB_REASON_NULL_BYTES)
|
|
165
187
|
try:
|
|
166
188
|
return raw_bytes.decode(BODY_FILE_ENCODING), None
|
|
167
189
|
except UnicodeDecodeError:
|
|
168
|
-
return
|
|
169
|
-
relative_path=relative_path,
|
|
170
|
-
reason=STAGED_BLOB_REASON_DECODE_FAILED,
|
|
171
|
-
)
|
|
190
|
+
return _unscannable_result(relative_path, STAGED_BLOB_REASON_DECODE_FAILED)
|
|
172
191
|
|
|
173
192
|
|
|
174
|
-
def
|
|
175
|
-
repository_root: Path,
|
|
193
|
+
def _scan_staged_path(
|
|
194
|
+
repository_root: Path, relative_path: str, all_allowlisted_values: frozenset[str]
|
|
176
195
|
) -> str | None:
|
|
196
|
+
"""Return a deny reason for one staged path, or None when it is clean."""
|
|
197
|
+
staged_text, unscannable_reason = read_staged_file_text(
|
|
198
|
+
repository_root, relative_path
|
|
199
|
+
)
|
|
200
|
+
if unscannable_reason is not None:
|
|
201
|
+
return unscannable_reason
|
|
202
|
+
if staged_text is None:
|
|
203
|
+
return _unscannable_result(relative_path, STAGED_BLOB_REASON_GIT_SHOW_FAILED)[1]
|
|
204
|
+
all_findings = [
|
|
205
|
+
each_finding
|
|
206
|
+
for each_finding in scan_text_for_pii(staged_text)
|
|
207
|
+
if each_finding.matched_text not in all_allowlisted_values
|
|
208
|
+
]
|
|
209
|
+
if not all_findings:
|
|
210
|
+
return None
|
|
211
|
+
return build_deny_reason(all_findings, f"staged commit ({relative_path})")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def evaluate_staged_commit(repository_root: Path) -> str | None:
|
|
177
215
|
"""Return a deny reason when staged content carries PII or is unscannable.
|
|
178
216
|
|
|
179
217
|
Fail-closed: git list/show failures and unscannable blobs deny the commit
|
|
180
|
-
rather than treating unread content as clean.
|
|
218
|
+
rather than treating unread content as clean. A value in the repository's
|
|
219
|
+
PII allowlist is dropped from the findings, so its commits may carry it.
|
|
181
220
|
|
|
182
221
|
Args:
|
|
183
222
|
repository_root: Repository whose index is about to be committed.
|
|
@@ -188,185 +227,45 @@ def evaluate_staged_commit(
|
|
|
188
227
|
all_relative_paths, list_failure_reason = list_staged_file_paths(repository_root)
|
|
189
228
|
if list_failure_reason is not None or all_relative_paths is None:
|
|
190
229
|
return list_failure_reason or STAGED_LIST_FAILURE_REASON
|
|
230
|
+
all_allowlisted_values = repository_allowlisted_values(repository_root)
|
|
191
231
|
for each_relative_path in all_relative_paths:
|
|
192
232
|
if is_path_exempt_from_pii_scan(each_relative_path):
|
|
193
233
|
continue
|
|
194
|
-
|
|
195
|
-
repository_root, each_relative_path
|
|
234
|
+
deny_reason = _scan_staged_path(
|
|
235
|
+
repository_root, each_relative_path, all_allowlisted_values
|
|
196
236
|
)
|
|
197
|
-
if
|
|
198
|
-
return
|
|
199
|
-
if staged_text is None:
|
|
200
|
-
return STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE.format(
|
|
201
|
-
relative_path=each_relative_path,
|
|
202
|
-
reason=STAGED_BLOB_REASON_GIT_SHOW_FAILED,
|
|
203
|
-
)
|
|
204
|
-
all_findings = scan_text_for_pii(staged_text)
|
|
205
|
-
if all_findings:
|
|
206
|
-
gate_surface = f"staged commit ({each_relative_path})"
|
|
207
|
-
return build_deny_reason(all_findings, gate_surface)
|
|
237
|
+
if deny_reason is not None:
|
|
238
|
+
return deny_reason
|
|
208
239
|
return None
|
|
209
240
|
|
|
210
241
|
|
|
211
|
-
def
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
WINDOWS_PATH_SEPARATOR,
|
|
216
|
-
):
|
|
217
|
-
stripped_url = stripped_url[:-1]
|
|
218
|
-
return stripped_url
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def _strip_trailing_git_suffix(origin_url: str) -> str:
|
|
222
|
-
if origin_url.lower().endswith(GIT_URL_SUFFIX):
|
|
223
|
-
return origin_url[: -len(GIT_URL_SUFFIX)]
|
|
224
|
-
return origin_url
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _normalized_origin_url(origin_url: str) -> str:
|
|
228
|
-
return _strip_trailing_git_suffix(_strip_trailing_path_separators(origin_url))
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _host_and_path_from_scheme_url(origin_url: str) -> tuple[str, str] | None:
|
|
232
|
-
parsed_url = urlparse(origin_url)
|
|
233
|
-
if parsed_url.scheme not in ALL_NETWORK_GIT_URL_SCHEMES:
|
|
234
|
-
return None
|
|
235
|
-
maybe_host = parsed_url.hostname
|
|
236
|
-
if maybe_host is None:
|
|
237
|
-
return None
|
|
238
|
-
try:
|
|
239
|
-
parsed_url.port
|
|
240
|
-
except ValueError:
|
|
241
|
-
return None
|
|
242
|
-
repository_path = parsed_url.path.replace(
|
|
243
|
-
WINDOWS_PATH_SEPARATOR, POSIX_PATH_SEPARATOR
|
|
244
|
-
).lstrip(POSIX_PATH_SEPARATOR)
|
|
245
|
-
return maybe_host.lower(), repository_path
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _host_and_path_from_scp_url(origin_url: str) -> tuple[str, str] | None:
|
|
249
|
-
if USERINFO_HOST_SEPARATOR not in origin_url:
|
|
250
|
-
return None
|
|
251
|
-
userinfo_and_host, separator, repository_path = origin_url.partition(
|
|
252
|
-
SCP_STYLE_PATH_SEPARATOR
|
|
253
|
-
)
|
|
254
|
-
if not separator or USERINFO_HOST_SEPARATOR not in userinfo_and_host:
|
|
255
|
-
return None
|
|
256
|
-
if not repository_path:
|
|
257
|
-
return None
|
|
258
|
-
host = userinfo_and_host.rsplit(USERINFO_HOST_SEPARATOR, maxsplit=1)[-1]
|
|
259
|
-
if not host:
|
|
260
|
-
return None
|
|
261
|
-
normalized_path = repository_path.replace(
|
|
262
|
-
WINDOWS_PATH_SEPARATOR, POSIX_PATH_SEPARATOR
|
|
263
|
-
)
|
|
264
|
-
return host.lower(), normalized_path
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def _host_and_repository_path_from_origin_url(
|
|
268
|
-
origin_url: str,
|
|
269
|
-
) -> tuple[str, str] | None:
|
|
270
|
-
normalized_url = _normalized_origin_url(origin_url)
|
|
271
|
-
if URL_SCHEME_SEPARATOR in normalized_url:
|
|
272
|
-
return _host_and_path_from_scheme_url(normalized_url)
|
|
273
|
-
return _host_and_path_from_scp_url(normalized_url)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def _owner_repo_slug_from_path(repository_path: str) -> str | None:
|
|
277
|
-
all_segments = [
|
|
278
|
-
each_segment
|
|
279
|
-
for each_segment in repository_path.split(POSIX_PATH_SEPARATOR)
|
|
280
|
-
if each_segment
|
|
281
|
-
]
|
|
282
|
-
if len(all_segments) < MINIMUM_OWNER_REPO_SEGMENT_COUNT:
|
|
283
|
-
return None
|
|
284
|
-
owner_name = all_segments[0]
|
|
285
|
-
repository_name = all_segments[1]
|
|
286
|
-
return POSIX_PATH_SEPARATOR.join((owner_name, repository_name)).lower()
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def _owner_repo_slug_from_origin_url(origin_url: str) -> str | None:
|
|
290
|
-
"""Return the lowercased owner/repo slug from a github.com origin URL.
|
|
291
|
-
|
|
292
|
-
::
|
|
293
|
-
|
|
294
|
-
https://github.com/Owner/Repo.git -> owner/repo
|
|
295
|
-
https://github.com/Owner/Repo -> owner/repo
|
|
296
|
-
https://github.com:443/Owner/Repo.git -> owner/repo
|
|
297
|
-
ssh://git@github.com:22/Owner/Repo.git -> owner/repo
|
|
298
|
-
git@github.com:Owner/Repo.git -> owner/repo
|
|
299
|
-
ssh://git@github.com/Owner/Repo.git -> owner/repo
|
|
300
|
-
https://github.com/Owner/Repo.git/ -> owner/repo
|
|
301
|
-
https://evil.test/Owner/Repo.git -> None
|
|
302
|
-
https://github.com:443.evil.test/Owner/Repo.git -> None
|
|
303
|
-
ssh://git@github.com:22.evil.test/Owner/Repo.git -> None
|
|
304
|
-
C:/repos/Owner/Repo -> None
|
|
305
|
-
|
|
306
|
-
Accepts only the exact host ``github.com`` (case-insensitive). Spoof hosts,
|
|
307
|
-
unparseable port authorities, and path-shaped origins return None so the
|
|
308
|
-
repository is never exempt.
|
|
309
|
-
|
|
310
|
-
Args:
|
|
311
|
-
origin_url: The ``remote.origin.url`` value, https or ssh form.
|
|
312
|
-
|
|
313
|
-
Returns:
|
|
314
|
-
The ``owner/repo`` slug in lowercase, or None when the host is not
|
|
315
|
-
exactly ``github.com``, the port authority is unparseable, or the URL
|
|
316
|
-
carries no owner/repo tail.
|
|
317
|
-
"""
|
|
318
|
-
maybe_host_and_path = _host_and_repository_path_from_origin_url(origin_url)
|
|
319
|
-
if maybe_host_and_path is None:
|
|
320
|
-
return None
|
|
321
|
-
host, repository_path = maybe_host_and_path
|
|
322
|
-
if host != GITHUB_COM_HOST:
|
|
323
|
-
return None
|
|
324
|
-
return _owner_repo_slug_from_path(repository_path)
|
|
325
|
-
|
|
242
|
+
def _evaluate_commit_staged_content(
|
|
243
|
+
bash_command: str, working_directory: str | None
|
|
244
|
+
) -> str | None:
|
|
245
|
+
"""Scan the staged content of the repository the commit command targets.
|
|
326
246
|
|
|
327
|
-
|
|
328
|
-
|
|
247
|
+
Resolves the repository from the command's ``-C``/``cd`` path, falling back
|
|
248
|
+
to the session working directory only when the command names none, so a
|
|
249
|
+
removed cwd never blocks a commit that targets a valid repo.
|
|
329
250
|
|
|
330
251
|
Args:
|
|
331
|
-
|
|
252
|
+
bash_command: The Bash or PowerShell tool command string.
|
|
253
|
+
working_directory: Session working directory fallback.
|
|
332
254
|
|
|
333
255
|
Returns:
|
|
334
|
-
|
|
335
|
-
readable origin remote or the URL carries no owner/repo tail.
|
|
256
|
+
Deny reason text, or None when the staged content is clean or exempt.
|
|
336
257
|
"""
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
return None
|
|
347
|
-
if completed_process.returncode != 0:
|
|
258
|
+
command_directory = compose_command_working_directory(bash_command)
|
|
259
|
+
attempted_directory = (
|
|
260
|
+
command_directory if command_directory is not None else working_directory
|
|
261
|
+
)
|
|
262
|
+
expanded_directory = expand_user_directory(attempted_directory)
|
|
263
|
+
repository_root = resolve_repository_root(expanded_directory)
|
|
264
|
+
if repository_root is None:
|
|
265
|
+
return refusal_reason_for_unresolved_repository(attempted_directory)
|
|
266
|
+
if _is_repository_exempt_from_pii_scan(repository_root):
|
|
348
267
|
return None
|
|
349
|
-
return
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def _is_repository_exempt_from_pii_scan(repository_root: Path) -> bool:
|
|
353
|
-
"""Report whether commits in *repository_root* skip the staged PII scan.
|
|
354
|
-
|
|
355
|
-
Exempt repositories are named by owner/repo slug through
|
|
356
|
-
``CLAUDE_PII_EXEMPT_REPOS`` or the ``pii_exempt_repositories`` list in the
|
|
357
|
-
git-ignored ``~/.claude/local-identity.json``. A repository without a readable
|
|
358
|
-
origin remote is never exempt (fail-closed to scanning).
|
|
359
|
-
|
|
360
|
-
Args:
|
|
361
|
-
repository_root: Repository whose index is about to be committed.
|
|
362
|
-
|
|
363
|
-
Returns:
|
|
364
|
-
True when the repository's origin slug is in the exempt set.
|
|
365
|
-
"""
|
|
366
|
-
origin_slug = _repository_origin_slug(repository_root)
|
|
367
|
-
if origin_slug is None:
|
|
368
|
-
return False
|
|
369
|
-
return origin_slug in pii_exempt_repository_slugs()
|
|
268
|
+
return evaluate_staged_commit(repository_root)
|
|
370
269
|
|
|
371
270
|
|
|
372
271
|
def evaluate_bash_command(
|
|
@@ -376,7 +275,8 @@ def evaluate_bash_command(
|
|
|
376
275
|
|
|
377
276
|
Args:
|
|
378
277
|
bash_command: The Bash or PowerShell tool command string.
|
|
379
|
-
working_directory:
|
|
278
|
+
working_directory: Session working directory, used only when the command
|
|
279
|
+
names no repository path.
|
|
380
280
|
|
|
381
281
|
Returns:
|
|
382
282
|
Deny reason text, or None when the command is clean or out of scope.
|
|
@@ -393,14 +293,26 @@ def evaluate_bash_command(
|
|
|
393
293
|
return post_deny_reason
|
|
394
294
|
if not is_git_commit_shell_command(bash_command):
|
|
395
295
|
return None
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
296
|
+
return _evaluate_commit_staged_content(bash_command, working_directory)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _evaluate_shell_tool(
|
|
300
|
+
all_tool_input: dict[str, object], payload_by_key: dict[str, object]
|
|
301
|
+
) -> str | None:
|
|
302
|
+
"""Resolve the command and working directory, then gate a shell tool call."""
|
|
303
|
+
command_value = all_tool_input.get("command", "")
|
|
304
|
+
if not isinstance(command_value, str) or not command_value:
|
|
402
305
|
return None
|
|
403
|
-
|
|
306
|
+
working_directory_value = all_tool_input.get("working_directory")
|
|
307
|
+
working_directory = (
|
|
308
|
+
working_directory_value
|
|
309
|
+
if isinstance(working_directory_value, str)
|
|
310
|
+
else None
|
|
311
|
+
)
|
|
312
|
+
if working_directory is None:
|
|
313
|
+
cwd_value = payload_by_key.get("cwd")
|
|
314
|
+
working_directory = cwd_value if isinstance(cwd_value, str) else None
|
|
315
|
+
return evaluate_bash_command(command_value, working_directory=working_directory)
|
|
404
316
|
|
|
405
317
|
|
|
406
318
|
def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
@@ -416,30 +328,12 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
|
416
328
|
tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
|
|
417
329
|
raw_tool_input = payload_by_key.get("tool_input", {})
|
|
418
330
|
all_tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
|
|
419
|
-
|
|
420
331
|
if tool_name in ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES:
|
|
421
332
|
return evaluate_write_edit_payload(tool_name, all_tool_input)
|
|
422
|
-
|
|
423
333
|
if tool_name in ALL_SHELL_TOOL_NAMES:
|
|
424
|
-
|
|
425
|
-
if not isinstance(command_value, str) or not command_value:
|
|
426
|
-
return None
|
|
427
|
-
working_directory_value = all_tool_input.get("working_directory")
|
|
428
|
-
working_directory = (
|
|
429
|
-
working_directory_value
|
|
430
|
-
if isinstance(working_directory_value, str)
|
|
431
|
-
else None
|
|
432
|
-
)
|
|
433
|
-
if working_directory is None:
|
|
434
|
-
cwd_value = payload_by_key.get("cwd")
|
|
435
|
-
working_directory = cwd_value if isinstance(cwd_value, str) else None
|
|
436
|
-
return evaluate_bash_command(
|
|
437
|
-
command_value, working_directory=working_directory
|
|
438
|
-
)
|
|
439
|
-
|
|
334
|
+
return _evaluate_shell_tool(all_tool_input, payload_by_key)
|
|
440
335
|
if tool_name.startswith(MCP_GITHUB_TOOL_PREFIX):
|
|
441
336
|
return evaluate_post_body_texts(extract_mcp_body_texts(all_tool_input))
|
|
442
|
-
|
|
443
337
|
return None
|
|
444
338
|
|
|
445
339
|
|
|
@@ -474,7 +368,7 @@ def main() -> None:
|
|
|
474
368
|
deny_reason = evaluate(payload_dictionary)
|
|
475
369
|
if deny_reason is None:
|
|
476
370
|
sys.exit(0)
|
|
477
|
-
|
|
371
|
+
sys.stdout.write(json.dumps(build_deny_payload(deny_reason)) + "\n")
|
|
478
372
|
sys.stdout.flush()
|
|
479
373
|
sys.exit(0)
|
|
480
374
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# pii_prevention_blocker_parts
|
|
2
|
+
|
|
3
|
+
The concern modules `pii_prevention_blocker.py` wires together. The entry hook
|
|
4
|
+
imports them and re-exports their surface for the test suite.
|
|
5
|
+
|
|
6
|
+
## Modules
|
|
7
|
+
|
|
8
|
+
| File | Purpose |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `repository_exemption.py` | Resolves a commit's origin owner/repo slug, decides whether the repository skips the staged PII scan, and resolves the exact values that repository allows past the scan |
|
|
11
|
+
| `repository_resolution.py` | Reads the repository a commit command targets — composing multiple `-C` values and a leading `cd`/`pushd` — and builds the deny reason naming the path when the repository root does not resolve |
|
|
12
|
+
| `__init__.py` | Package marker |
|
|
13
|
+
|
|
14
|
+
## Subdirectories
|
|
15
|
+
|
|
16
|
+
| Entry | Description |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `config/` | The repository-resolution deny-message template and the session-cwd label (`repository_resolution_constants.py`) |
|
|
19
|
+
|
|
20
|
+
## Running tests
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m pytest packages/claude-dev-env/hooks/blocking/tests/
|
|
24
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Decomposed concern modules the ``pii_prevention_blocker`` entry hook wires together."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration home for the pii_prevention_blocker parts constants."""
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Named constants for the commit repository-resolution deny message.
|
|
2
|
+
|
|
3
|
+
The template carries an ``{attempted_path}`` slot so the refusal names the exact
|
|
4
|
+
directory whose git repository root did not resolve. The cwd label fills that
|
|
5
|
+
slot when the command named no path and the gate fell back to the session
|
|
6
|
+
working directory.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
REPOSITORY_ROOT_UNRESOLVED_REASON_TEMPLATE: str = (
|
|
12
|
+
"BLOCKED [pii_prevention_blocker]: could not resolve the git repository root "
|
|
13
|
+
"for a commit command targeting {attempted_path} (not a git work tree, git "
|
|
14
|
+
"missing, or bad -C or cd path). Refuse commit until the repository root is "
|
|
15
|
+
"resolvable."
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
REPOSITORY_ROOT_UNRESOLVED_CWD_LABEL: str = "the session working directory"
|
|
19
|
+
|
|
20
|
+
ALL_DIRECTORY_CHANGE_COMMAND_NAMES: frozenset[str] = frozenset(
|
|
21
|
+
{"cd", "pushd", "set-location", "sl"}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
ALL_DIRECTORY_CHANGE_PATH_OPTION_NAMES: frozenset[str] = frozenset(
|
|
25
|
+
{"-path", "-literalpath"}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DIRECTORY_CHANGE_OPTION_TERMINATOR: str = "--"
|