claude-dev-env 1.94.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 +41 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
- package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
- 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/CLAUDE.md +10 -1
- package/bin/ever-shipped-skills.mjs +73 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +140 -7
- package/bin/install.prune.test.mjs +457 -0
- package/docs/CODE_RULES.md +1 -1
- 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_enforcer.py +4 -0
- 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 +83 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- 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/plain_language_blocker.py +138 -4
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +97 -601
- 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_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -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_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -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 +4 -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/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +52 -4
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- 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 +6 -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 +202 -18
- 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/README.md +2 -3
- 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/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- 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 +32 -22
- package/skills/orchestrator-refresh/SKILL.md +12 -8
- package/skills/pr-converge/CLAUDE.md +1 -1
- 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-fix-protocol/SKILL.md +65 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/skill-builder/references/skill-modularity.md +1 -1
- package/skills/team-advisor/SKILL.md +16 -12
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Behavioral tests for the moved repository-exemption helpers.
|
|
2
|
+
|
|
3
|
+
Covers the origin-URL slug parser and the per-repository exemption decision::
|
|
4
|
+
|
|
5
|
+
https://github.com/Owner/Repo.git -> owner/repo
|
|
6
|
+
https://evil.test/Owner/Repo.git -> None
|
|
7
|
+
ok: origin slug in CLAUDE_PII_EXEMPT_REPOS -> exempt (True)
|
|
8
|
+
flag: origin slug absent from the exempt set -> scanned (False)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
from pii_prevention_blocker_parts.repository_exemption import (
|
|
19
|
+
_is_repository_exempt_from_pii_scan,
|
|
20
|
+
_owner_repo_slug_from_origin_url,
|
|
21
|
+
repository_allowlisted_values,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_ALLOWED_VALUE = "owner.fixture" + "@" + "acme-corp" + ".example" + ".io"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _write_allowlist_identity(identity_path: Path, slug: str) -> None:
|
|
28
|
+
identity_path.write_text(
|
|
29
|
+
json.dumps({"pii_allowlisted_values": {slug: [_ALLOWED_VALUE]}}),
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_repository_allowlisted_values_returns_the_mapped_values(
|
|
35
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
36
|
+
) -> None:
|
|
37
|
+
identity_path = tmp_path / "local-identity.json"
|
|
38
|
+
_write_allowlist_identity(identity_path, "AllowOwner/allow-repo")
|
|
39
|
+
monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
|
|
40
|
+
repository_root = tmp_path / "repo"
|
|
41
|
+
_init_repo_with_github_origin(repository_root, "AllowOwner/allow-repo")
|
|
42
|
+
assert repository_allowlisted_values(repository_root) == frozenset({_ALLOWED_VALUE})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_repository_allowlisted_values_empty_for_an_unmapped_repo(
|
|
46
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
47
|
+
) -> None:
|
|
48
|
+
identity_path = tmp_path / "local-identity.json"
|
|
49
|
+
_write_allowlist_identity(identity_path, "AllowOwner/allow-repo")
|
|
50
|
+
monkeypatch.setenv("CLAUDE_LOCAL_IDENTITY_PATH", str(identity_path))
|
|
51
|
+
repository_root = tmp_path / "repo"
|
|
52
|
+
_init_repo_with_github_origin(repository_root, "OtherOwner/other-repo")
|
|
53
|
+
assert repository_allowlisted_values(repository_root) == frozenset()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_github_https_origin_yields_lowercased_owner_repo_slug() -> None:
|
|
57
|
+
assert _owner_repo_slug_from_origin_url("https://github.com/Owner/Repo.git") == "owner/repo"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_github_ssh_origin_yields_lowercased_owner_repo_slug() -> None:
|
|
61
|
+
assert _owner_repo_slug_from_origin_url("git@github.com:Owner/Repo.git") == "owner/repo"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_spoofed_host_origin_yields_no_slug() -> None:
|
|
65
|
+
assert _owner_repo_slug_from_origin_url("https://evil.test/Owner/Repo.git") is None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _init_repo_with_github_origin(repository_root: Path, origin_slug: str) -> None:
|
|
69
|
+
repository_root.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
|
|
71
|
+
origin_url = "https://github.com/" + origin_slug + ".git"
|
|
72
|
+
subprocess.run(
|
|
73
|
+
["git", "remote", "add", "origin", origin_url],
|
|
74
|
+
cwd=repository_root,
|
|
75
|
+
check=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_repository_with_exempt_origin_is_exempt(
|
|
80
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
81
|
+
) -> None:
|
|
82
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
83
|
+
repository_root = tmp_path / "repo"
|
|
84
|
+
_init_repo_with_github_origin(repository_root, "ExemptOwner/exempt-repo")
|
|
85
|
+
assert _is_repository_exempt_from_pii_scan(repository_root) is True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_repository_with_unlisted_origin_is_scanned(
|
|
89
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
90
|
+
) -> None:
|
|
91
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
92
|
+
repository_root = tmp_path / "repo"
|
|
93
|
+
_init_repo_with_github_origin(repository_root, "OtherOwner/other-repo")
|
|
94
|
+
assert _is_repository_exempt_from_pii_scan(repository_root) is False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_repository_without_origin_is_never_exempt(tmp_path: Path) -> None:
|
|
98
|
+
repository_root = tmp_path / "repo"
|
|
99
|
+
repository_root.mkdir()
|
|
100
|
+
subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
|
|
101
|
+
assert _is_repository_exempt_from_pii_scan(repository_root) is False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_owner_repo_slug_strips_a_trailing_slash_origin() -> None:
|
|
105
|
+
assert _owner_repo_slug_from_origin_url("https://github.com/Owner/Repo/") == "owner/repo"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Behavioral tests for the commit repository-path resolution helpers.
|
|
2
|
+
|
|
3
|
+
Covers reading the repository a commit command targets, so the staged PII scan
|
|
4
|
+
runs against the named path rather than the session working directory::
|
|
5
|
+
|
|
6
|
+
git -C /a -C sub commit -> /a/sub (multiple -C compose)
|
|
7
|
+
git -C /a/b -C /c/d commit -> /c/d (a later absolute -C resets)
|
|
8
|
+
cd "/my repo" && git commit -> /my repo (quoted cd path, shlex-parsed)
|
|
9
|
+
git commit -> None (runs in the session cwd)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from pii_prevention_blocker_parts.repository_resolution import (
|
|
17
|
+
compose_command_working_directory,
|
|
18
|
+
expand_user_directory,
|
|
19
|
+
refusal_reason_for_unresolved_repository,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_single_absolute_dash_c_is_the_target() -> None:
|
|
24
|
+
assert compose_command_working_directory("git -C /repo commit -m x") == "/repo"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_relative_dash_c_composes_onto_a_leading_absolute_dash_c() -> None:
|
|
28
|
+
composed = compose_command_working_directory("git -C /a -C sub commit -m x")
|
|
29
|
+
assert composed is not None
|
|
30
|
+
assert Path(composed) == Path("/a") / "sub"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_later_absolute_dash_c_resets_the_composition() -> None:
|
|
34
|
+
assert compose_command_working_directory("git -C /a/b -C /c/d commit -m x") == "/c/d"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_quoted_dash_c_with_spaces_is_read_whole() -> None:
|
|
38
|
+
assert compose_command_working_directory('git -C "/my repo" commit -m x') == "/my repo"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_leading_cd_supplies_the_target_when_no_dash_c() -> None:
|
|
42
|
+
assert compose_command_working_directory("cd /work/repo && git commit -m x") == "/work/repo"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_quoted_cd_path_with_spaces_is_read_whole() -> None:
|
|
46
|
+
assert compose_command_working_directory('cd "/my repo" && git commit -m x') == "/my repo"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_plain_commit_names_no_target() -> None:
|
|
50
|
+
assert compose_command_working_directory("git commit -m x") is None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_expand_user_directory_expands_tilde() -> None:
|
|
54
|
+
expanded = expand_user_directory("~/repo")
|
|
55
|
+
assert expanded is not None
|
|
56
|
+
assert not expanded.startswith("~")
|
|
57
|
+
assert Path(expanded).name == "repo"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_expand_user_directory_passes_none_through() -> None:
|
|
61
|
+
assert expand_user_directory(None) is None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_refusal_names_the_attempted_path() -> None:
|
|
65
|
+
reason = refusal_reason_for_unresolved_repository("/gone/repo")
|
|
66
|
+
assert "repository root" in reason
|
|
67
|
+
assert "/gone/repo" in reason
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_refusal_names_the_session_cwd_when_no_path_given() -> None:
|
|
71
|
+
reason = refusal_reason_for_unresolved_repository(None)
|
|
72
|
+
assert "repository root" in reason
|
|
73
|
+
assert "session working directory" in reason
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_windows_git_exe_dash_c_is_the_target() -> None:
|
|
77
|
+
assert (
|
|
78
|
+
compose_command_working_directory(r'git.exe -C "/srv/repo" commit -m x')
|
|
79
|
+
== "/srv/repo"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_leading_cd_with_git_exe_supplies_the_target() -> None:
|
|
84
|
+
assert (
|
|
85
|
+
compose_command_working_directory("cd /work/repo && git.exe commit -m x")
|
|
86
|
+
== "/work/repo"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_leading_pushd_with_git_exe_supplies_the_target() -> None:
|
|
91
|
+
assert (
|
|
92
|
+
compose_command_working_directory("pushd /work/repo; git.exe commit -m x")
|
|
93
|
+
== "/work/repo"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_quoted_cd_path_with_git_exe_is_read_whole() -> None:
|
|
98
|
+
assert (
|
|
99
|
+
compose_command_working_directory('cd "/my repo" && git.exe commit -m x')
|
|
100
|
+
== "/my repo"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_dash_c_wins_over_leading_cd() -> None:
|
|
105
|
+
assert (
|
|
106
|
+
compose_command_working_directory("cd /ignored && git -C /named commit -m x")
|
|
107
|
+
== "/named"
|
|
108
|
+
)
|
|
@@ -309,7 +309,7 @@ class AttachmentRecordIterator:
|
|
|
309
309
|
|
|
310
310
|
def __iter__(self) -> Iterator[tuple[dict[str, object], int, int]]:
|
|
311
311
|
try:
|
|
312
|
-
jsonl_file_handle = io.open(self._jsonl_file_path, "rb")
|
|
312
|
+
jsonl_file_handle = io.open(self._jsonl_file_path, "rb") # noqa: SIM115
|
|
313
313
|
except (FileNotFoundError, OSError):
|
|
314
314
|
self.final_line_number = self._start_line_number
|
|
315
315
|
self.final_byte_offset = self._start_offset
|
|
@@ -419,14 +419,13 @@ def save_offsets(
|
|
|
419
419
|
state_file_parent = os.path.dirname(state_file_path)
|
|
420
420
|
if state_file_parent:
|
|
421
421
|
os.makedirs(state_file_parent, exist_ok=True)
|
|
422
|
-
|
|
422
|
+
with tempfile.NamedTemporaryFile(
|
|
423
423
|
mode="w",
|
|
424
424
|
encoding="utf-8",
|
|
425
425
|
dir=state_file_parent or None,
|
|
426
426
|
delete=False,
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
try:
|
|
427
|
+
) as temporary_file_handle:
|
|
428
|
+
temporary_file_path = temporary_file_handle.name
|
|
430
429
|
try:
|
|
431
430
|
json.dump(
|
|
432
431
|
offset_by_jsonl_path,
|
|
@@ -436,8 +435,14 @@ def save_offsets(
|
|
|
436
435
|
)
|
|
437
436
|
temporary_file_handle.flush()
|
|
438
437
|
os.fsync(temporary_file_handle.fileno())
|
|
439
|
-
|
|
438
|
+
except Exception:
|
|
440
439
|
temporary_file_handle.close()
|
|
440
|
+
try:
|
|
441
|
+
os.unlink(temporary_file_path)
|
|
442
|
+
except OSError:
|
|
443
|
+
pass
|
|
444
|
+
raise
|
|
445
|
+
try:
|
|
441
446
|
os.replace(temporary_file_path, state_file_path)
|
|
442
447
|
except Exception:
|
|
443
448
|
try:
|
|
@@ -468,15 +473,12 @@ def _acquire_offsets_lock(state_file_path: str) -> Iterator[None]:
|
|
|
468
473
|
lock_parent_directory = os.path.dirname(lock_file_path)
|
|
469
474
|
if lock_parent_directory:
|
|
470
475
|
os.makedirs(lock_parent_directory, exist_ok=True)
|
|
471
|
-
|
|
472
|
-
try:
|
|
476
|
+
with io.open(lock_file_path, "a+", encoding="utf-8") as lock_file_handle:
|
|
473
477
|
_lock_file_handle_blocking(lock_file_handle)
|
|
474
478
|
try:
|
|
475
479
|
yield
|
|
476
480
|
finally:
|
|
477
481
|
_unlock_file_handle(lock_file_handle)
|
|
478
|
-
finally:
|
|
479
|
-
lock_file_handle.close()
|
|
480
482
|
|
|
481
483
|
|
|
482
484
|
def _lock_file_handle_blocking(lock_file_handle: IO[str]) -> None:
|
|
@@ -10,7 +10,6 @@ When you commit in a submodule, this hook:
|
|
|
10
10
|
This prevents the "lost work" issue where submodule commits aren't tracked by parent.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import os
|
|
14
13
|
import subprocess
|
|
15
14
|
import sys
|
|
16
15
|
from pathlib import Path
|
|
@@ -21,7 +20,7 @@ def run_git(*args: str, cwd: Path | None = None) -> str:
|
|
|
21
20
|
result = subprocess.run(
|
|
22
21
|
["git"] + list(args),
|
|
23
22
|
cwd=cwd,
|
|
24
|
-
capture_output=True,
|
|
23
|
+
check=False, capture_output=True,
|
|
25
24
|
text=True,
|
|
26
25
|
)
|
|
27
26
|
return result.stdout.strip()
|
|
@@ -74,7 +73,7 @@ def main() -> int:
|
|
|
74
73
|
|
|
75
74
|
diff_result = subprocess.run(
|
|
76
75
|
["git", "diff", "--cached", "--quiet"],
|
|
77
|
-
cwd=parent_repo,
|
|
76
|
+
check=False, cwd=parent_repo,
|
|
78
77
|
)
|
|
79
78
|
|
|
80
79
|
if diff_result.returncode == 0:
|
|
@@ -89,7 +88,7 @@ Co-Authored-By: Claude <noreply@anthropic.com>"""
|
|
|
89
88
|
|
|
90
89
|
subprocess.run(
|
|
91
90
|
["git", "commit", "-m", full_commit_msg],
|
|
92
|
-
cwd=parent_repo,
|
|
91
|
+
check=False, cwd=parent_repo,
|
|
93
92
|
)
|
|
94
93
|
|
|
95
94
|
print("Parent updated successfully.")
|
|
@@ -32,13 +32,14 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
32
32
|
| `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
|
|
33
33
|
| `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
|
|
34
34
|
| `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
|
|
35
|
+
| `harness_scratchpad_constants.py` | Fixed path components (`claude-` user-directory prefix, separator replacement, `scratchpad` leaf name) and payload keys that rebuild the harness session scratchpad directory for the code-rules and TDD gate exemptions |
|
|
35
36
|
| `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
|
|
36
37
|
| `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
|
|
37
38
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
38
39
|
| `hosted_hook_runner.py` | `run_hook_capturing_output()` — shared runner that runs one dispatcher-hosted hook in-process via runpy and returns its captured stdout and crash flag |
|
|
39
40
|
| `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
|
|
40
41
|
| `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
|
|
41
|
-
| `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults),
|
|
42
|
+
| `local_identity.py` | Loader for local machine values: NAS host, ssh user, and ssh port the ssh enforcer guards (`CLAUDE_NAS_*` / `~/.claude/local-identity.json` with placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`, keyed by owner/repo slug) read from the git-ignored local file whose path `CLAUDE_LOCAL_IDENTITY_PATH` may point elsewhere; also composes the ssh enforcer's two deny messages that quote the NAS values |
|
|
42
43
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
43
44
|
| `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
|
|
44
45
|
| `nas_ssh_binary_enforcer_constants.py` | Bash tool name, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, and the batch-mode pattern for the NAS ssh binary enforcer |
|
|
@@ -60,6 +61,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
60
61
|
| `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
|
|
61
62
|
| `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
|
|
62
63
|
| `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
|
|
64
|
+
| `sensitive_file_protector_constants.py` | Sensitive filename patterns, the committed-template suffixes that earn an exemption, the write/edit tool names, and the deny decision and message template for `sensitive_file_protector` |
|
|
63
65
|
| `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
|
|
64
66
|
| `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
|
|
65
67
|
| `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
|
|
@@ -85,5 +87,5 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
85
87
|
- Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
|
|
86
88
|
- Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
|
|
87
89
|
- `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they are shared across many hooks.
|
|
88
|
-
- `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` to resolve private NAS values (with committed placeholder defaults)
|
|
90
|
+
- `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` (its path overridable via `CLAUDE_LOCAL_IDENTITY_PATH`) to resolve private NAS values (with committed placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`).
|
|
89
91
|
|
|
@@ -10,7 +10,6 @@ module.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
ALL_CONFIG_DIRECTORY_NAMES = frozenset(
|
|
15
14
|
{
|
|
16
15
|
"anthropic_plan_scripts_constants",
|
|
@@ -23,6 +22,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
|
|
|
23
22
|
"pr_converge_scripts_constants",
|
|
24
23
|
"bugteam_scripts_constants",
|
|
25
24
|
"usage_pause_constants",
|
|
25
|
+
"codex_review_scripts_constants",
|
|
26
26
|
"dev_env_scripts_constants",
|
|
27
27
|
}
|
|
28
28
|
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Constants for resolving the Claude Code harness session scratchpad directory.
|
|
2
|
+
|
|
3
|
+
The harness gives each session a throwaway scratch directory it announces in
|
|
4
|
+
the session's system prompt, laid out as
|
|
5
|
+
``<tempdir>/claude-<uid>/<mangled-cwd>/<session-id>/scratchpad``. These constants
|
|
6
|
+
name the fixed path components the code-rules and TDD gates read to rebuild that
|
|
7
|
+
directory from the signals a hook process can see.
|
|
8
|
+
|
|
9
|
+
The exemption is POSIX-only: without ``os.getuid`` (Windows) it never fires,
|
|
10
|
+
so full enforcement stays in place there.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX: str = "claude-"
|
|
14
|
+
HARNESS_SCRATCHPAD_PATH_SEPARATOR_REPLACEMENT: str = "-"
|
|
15
|
+
HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME: str = "scratchpad"
|
|
16
|
+
HOOK_PAYLOAD_SESSION_ID_KEY: str = "session_id"
|
|
17
|
+
HOOK_PAYLOAD_WORKING_DIRECTORY_KEY: str = "cwd"
|
|
@@ -27,9 +27,11 @@ import os
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
29
|
NAS_HOST_ENV_VAR = "CLAUDE_NAS_HOST"
|
|
30
|
+
LOCAL_IDENTITY_PATH_ENV_VAR = "CLAUDE_LOCAL_IDENTITY_PATH"
|
|
30
31
|
PII_EXEMPT_REPOS_ENV_VAR = "CLAUDE_PII_EXEMPT_REPOS"
|
|
31
32
|
PII_EXEMPT_REPOS_JSON_KEY = "pii_exempt_repositories"
|
|
32
33
|
PII_EXEMPT_REPOS_SEPARATOR = ","
|
|
34
|
+
PII_ALLOWLISTED_VALUES_JSON_KEY = "pii_allowlisted_values"
|
|
33
35
|
NAS_SSH_USER_ENV_VAR = "CLAUDE_NAS_SSH_USER"
|
|
34
36
|
NAS_SSH_PORT_ENV_VAR = "CLAUDE_NAS_SSH_PORT"
|
|
35
37
|
NAS_JSON_KEY = "nas"
|
|
@@ -46,6 +48,9 @@ NAS_SSH_RULE_REFERENCE = "~/.claude/rules/nas-ssh-invocation.md"
|
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def _local_identity_file_path() -> Path:
|
|
51
|
+
path_override = os.environ.get(LOCAL_IDENTITY_PATH_ENV_VAR)
|
|
52
|
+
if path_override:
|
|
53
|
+
return Path(path_override)
|
|
49
54
|
return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / LOCAL_IDENTITY_FILE_NAME
|
|
50
55
|
|
|
51
56
|
|
|
@@ -73,13 +78,22 @@ def _normalized_slug_set(all_raw_slugs: list[object]) -> frozenset[str]:
|
|
|
73
78
|
)
|
|
74
79
|
|
|
75
80
|
|
|
81
|
+
def _environment_exempt_slugs() -> frozenset[str]:
|
|
82
|
+
"""Return the exempt slugs named by ``CLAUDE_PII_EXEMPT_REPOS``, or empty."""
|
|
83
|
+
slugs_from_environment = os.environ.get(PII_EXEMPT_REPOS_ENV_VAR)
|
|
84
|
+
if slugs_from_environment is None:
|
|
85
|
+
return frozenset()
|
|
86
|
+
return _normalized_slug_set(
|
|
87
|
+
list(slugs_from_environment.split(PII_EXEMPT_REPOS_SEPARATOR))
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
76
91
|
def pii_exempt_repository_slugs() -> frozenset[str]:
|
|
77
92
|
"""Return owner/repo slugs whose commits skip the staged PII scan.
|
|
78
93
|
|
|
79
94
|
::
|
|
80
95
|
|
|
81
96
|
CLAUDE_PII_EXEMPT_REPOS="Owner/repo-a, Other/b" -> {"owner/repo-a", "other/b"}
|
|
82
|
-
CLAUDE_PII_EXEMPT_REPOS=" , , " + file lists A -> {"a"} (whitespace falls through)
|
|
83
97
|
(env unset, file lists ["Owner/repo-a"]) -> {"owner/repo-a"}
|
|
84
98
|
(env unset, no file) -> frozenset()
|
|
85
99
|
|
|
@@ -91,13 +105,9 @@ def pii_exempt_repository_slugs() -> frozenset[str]:
|
|
|
91
105
|
Returns:
|
|
92
106
|
Lowercased ``owner/repo`` slugs exempt from staged-commit PII scanning.
|
|
93
107
|
"""
|
|
94
|
-
|
|
95
|
-
if
|
|
96
|
-
|
|
97
|
-
list(slugs_from_environment.split(PII_EXEMPT_REPOS_SEPARATOR))
|
|
98
|
-
)
|
|
99
|
-
if normalized_environment_slugs:
|
|
100
|
-
return normalized_environment_slugs
|
|
108
|
+
environment_slugs = _environment_exempt_slugs()
|
|
109
|
+
if environment_slugs:
|
|
110
|
+
return environment_slugs
|
|
101
111
|
stored_slugs = _identity_dictionary_from_local_file().get(
|
|
102
112
|
PII_EXEMPT_REPOS_JSON_KEY
|
|
103
113
|
)
|
|
@@ -106,6 +116,47 @@ def pii_exempt_repository_slugs() -> frozenset[str]:
|
|
|
106
116
|
return _normalized_slug_set(stored_slugs)
|
|
107
117
|
|
|
108
118
|
|
|
119
|
+
def _normalized_allowlisted_values(all_raw_values: object) -> frozenset[str]:
|
|
120
|
+
"""Return the non-empty string members of a raw values list, or empty."""
|
|
121
|
+
if not isinstance(all_raw_values, list):
|
|
122
|
+
return frozenset()
|
|
123
|
+
return frozenset(
|
|
124
|
+
each_value
|
|
125
|
+
for each_value in all_raw_values
|
|
126
|
+
if isinstance(each_value, str) and each_value
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def pii_allowlisted_values_by_repository() -> dict[str, frozenset[str]]:
|
|
131
|
+
"""Return the exact literal values each repository allows past the PII scan.
|
|
132
|
+
|
|
133
|
+
::
|
|
134
|
+
|
|
135
|
+
file {"Owner/Repo": ["user@example.com"]} -> {"owner/repo": {"user@example.com"}}
|
|
136
|
+
(env unset, no file, or bad json) -> {}
|
|
137
|
+
|
|
138
|
+
Keyed by lowercased ``owner/repo`` slug so matching is case-insensitive; the
|
|
139
|
+
values stay exact. The mapping lives under ``pii_allowlisted_values`` in the
|
|
140
|
+
git-ignored ``~/.claude/local-identity.json`` so a private repository's real
|
|
141
|
+
fixture addresses stay out of this published tree.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Lowercased ``owner/repo`` slug to the frozenset of exact allowed values.
|
|
145
|
+
"""
|
|
146
|
+
stored_mapping = _identity_dictionary_from_local_file().get(
|
|
147
|
+
PII_ALLOWLISTED_VALUES_JSON_KEY
|
|
148
|
+
)
|
|
149
|
+
if not isinstance(stored_mapping, dict):
|
|
150
|
+
return {}
|
|
151
|
+
all_values_by_slug: dict[str, frozenset[str]] = {}
|
|
152
|
+
for each_raw_slug, each_raw_values in stored_mapping.items():
|
|
153
|
+
normalized_slug = each_raw_slug.strip().lower() if isinstance(each_raw_slug, str) else ""
|
|
154
|
+
allowlisted_values = _normalized_allowlisted_values(each_raw_values)
|
|
155
|
+
if normalized_slug and allowlisted_values:
|
|
156
|
+
all_values_by_slug[normalized_slug] = allowlisted_values
|
|
157
|
+
return all_values_by_slug
|
|
158
|
+
|
|
159
|
+
|
|
109
160
|
def nas_host() -> str:
|
|
110
161
|
"""Return the NAS host the ssh enforcer guards.
|
|
111
162
|
|
|
@@ -293,12 +293,6 @@ STAGED_LIST_FAILURE_REASON: str = (
|
|
|
293
293
|
"(git diff --cached failed). Refuse commit until the index is readable."
|
|
294
294
|
)
|
|
295
295
|
|
|
296
|
-
REPOSITORY_ROOT_UNRESOLVED_REASON: str = (
|
|
297
|
-
"BLOCKED [pii_prevention_blocker]: could not resolve the git repository root "
|
|
298
|
-
"for a commit command (not a git work tree, git missing, or bad -C path). "
|
|
299
|
-
"Refuse commit until the repository root is resolvable."
|
|
300
|
-
)
|
|
301
|
-
|
|
302
296
|
STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE: str = (
|
|
303
297
|
"BLOCKED [pii_prevention_blocker]: staged file '{relative_path}' could not be "
|
|
304
298
|
"scanned for PII ({reason}). Refuse commit, shrink the blob, or keep binary "
|
|
@@ -289,6 +289,11 @@ MARKDOWN_EXTENSION: str = ".md"
|
|
|
289
289
|
ASK_USER_QUESTION_TOOL_NAME: str = "AskUserQuestion"
|
|
290
290
|
ALL_WRITE_EDIT_TOOL_NAMES: frozenset[str] = frozenset({"Write", "Edit", "MultiEdit"})
|
|
291
291
|
|
|
292
|
+
DOT_CLAUDE_DIRECTORY_NAME: str = ".claude"
|
|
293
|
+
PROJECT_ALLOWLIST_FILENAME: str = "plain-language-allow.json"
|
|
294
|
+
PROJECT_ROOT_WALK_LIMIT: int = 40
|
|
295
|
+
REPOSITORY_MARKER_NAME: str = ".git"
|
|
296
|
+
|
|
292
297
|
USER_FACING_PLAIN_LANGUAGE_NOTICE: str = (
|
|
293
298
|
"Plain-language check: replace each flagged formal word with the simpler "
|
|
294
299
|
"word named in the reason."
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Constants for the ``sensitive_file_protector`` PreToolUse hook.
|
|
2
|
+
|
|
3
|
+
The hook denies a Write or Edit whose target basename names a live secret or a
|
|
4
|
+
lock file, and steps aside for a placeholders-only committed template.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
ALL_SENSITIVE_PATTERNS = (
|
|
10
|
+
".env",
|
|
11
|
+
".env.*",
|
|
12
|
+
"*.env",
|
|
13
|
+
"*.pem",
|
|
14
|
+
"*.key",
|
|
15
|
+
"*.p12",
|
|
16
|
+
"*.pfx",
|
|
17
|
+
"credentials.json",
|
|
18
|
+
"secrets.json",
|
|
19
|
+
"id_rsa",
|
|
20
|
+
"id_ed25519",
|
|
21
|
+
"package-lock.json",
|
|
22
|
+
"yarn.lock",
|
|
23
|
+
"Pipfile.lock",
|
|
24
|
+
"poetry.lock",
|
|
25
|
+
"pnpm-lock.yaml",
|
|
26
|
+
"composer.lock",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
ALL_TEMPLATE_SUFFIXES = (".example", ".sample", ".template")
|
|
30
|
+
|
|
31
|
+
ALL_WRITE_EDIT_TOOLS = ("Write", "Edit")
|
|
32
|
+
|
|
33
|
+
HOOK_SCRIPT_NAME = "sensitive_file_protector.py"
|
|
34
|
+
|
|
35
|
+
HOOK_EVENT_NAME = "PreToolUse"
|
|
36
|
+
|
|
37
|
+
DENY_DECISION = "deny"
|
|
38
|
+
|
|
39
|
+
DENY_REASON_TEMPLATE = (
|
|
40
|
+
"BLOCKED: Sensitive file '{filename}' (pattern: '{matched_pattern}'). "
|
|
41
|
+
"Edit manually outside Claude Code."
|
|
42
|
+
)
|