claude-dev-env 1.93.1 → 1.95.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 +35 -27
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -2
- package/_shared/advisor/scripts/model_tier_run_validator.py +23 -15
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +81 -17
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/audit-contract.md +1 -1
- package/_shared/pr-loop/gh-payloads.md +3 -3
- package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
- package/_shared/pr-loop/scripts/README.md +2 -2
- package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
- package/_shared/pr-loop/state-schema.md +3 -22
- package/bin/CLAUDE.md +11 -2
- package/bin/ever-shipped-skills.mjs +70 -0
- package/bin/install.mjs +138 -13
- package/bin/install.prune.test.mjs +457 -0
- package/bin/install.test.mjs +10 -4
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/code_rules_enforcer.py +4 -0
- package/hooks/blocking/code_rules_shared.py +82 -0
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- package/hooks/blocking/pii_commit_command.py +398 -0
- package/hooks/blocking/pii_payload_scan.py +133 -0
- package/hooks/blocking/pii_prevention_blocker.py +151 -449
- package/hooks/blocking/plain_language_blocker.py +138 -4
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +9 -2
- 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_pii_prevention_blocker.py +299 -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_scratchpad.py +105 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +4 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/hooks/pyproject.toml +75 -4
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -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/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 +10 -1
- package/scripts/Get-SessionAccount.ps1 +210 -0
- package/scripts/tests/CLAUDE.md +15 -1
- package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
- package/skills/CLAUDE.md +6 -27
- package/skills/anthropic-plan/SKILL.md +1 -13
- package/skills/autoconverge/CLAUDE.md +5 -2
- package/skills/autoconverge/SKILL.md +112 -387
- package/skills/autoconverge/reference/CLAUDE.md +4 -1
- package/skills/autoconverge/reference/closing-report.md +9 -2
- package/skills/autoconverge/reference/convergence.md +26 -22
- package/skills/autoconverge/reference/headless-safety.md +44 -0
- package/skills/autoconverge/reference/multi-pr.md +88 -0
- package/skills/autoconverge/reference/self-closing-loop.md +84 -0
- package/skills/autoconverge/reference/stop-conditions.md +21 -13
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
- package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +286 -96
- package/skills/bugteam/CLAUDE.md +1 -1
- package/skills/bugteam/CONSTRAINTS.md +3 -3
- package/skills/bugteam/PROMPTS.md +8 -19
- package/skills/bugteam/SKILL.md +13 -14
- package/skills/bugteam/reference/CLAUDE.md +1 -1
- package/skills/bugteam/reference/README.md +2 -3
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
- package/skills/bugteam/reference/github-pr-reviews.md +56 -38
- package/skills/bugteam/reference/team-setup.md +11 -11
- package/skills/bugteam/scripts/CLAUDE.md +6 -6
- package/skills/bugteam/scripts/README.md +20 -8
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
- package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
- package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
- 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/copilot-finding-triage/SKILL.md +80 -28
- package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
- package/skills/copilot-finding-triage/templates/notification.md +8 -3
- package/skills/everything-search/SKILL.md +1 -2
- package/skills/grokify/SKILL.md +58 -0
- package/skills/grokify/templates/handoff-template.md +92 -0
- package/skills/orchestrator/SKILL.md +35 -25
- package/skills/orchestrator-refresh/SKILL.md +15 -11
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +132 -233
- package/skills/pr-converge/reference/convergence-gates.md +102 -66
- package/skills/pr-converge/reference/examples.md +37 -25
- package/skills/pr-converge/reference/fix-protocol.md +7 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
- package/skills/pr-converge/reference/per-tick.md +64 -41
- package/skills/pr-converge/reference/state-schema.md +27 -2
- package/skills/pr-converge/scripts/CLAUDE.md +8 -0
- package/skills/pr-converge/scripts/README.md +4 -3
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
- package/skills/pr-converge/scripts/check_convergence.py +189 -645
- package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
- package/skills/pr-converge/scripts/conftest.py +8 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
- package/skills/pr-fix-protocol/SKILL.md +20 -20
- package/skills/privacy-hygiene/SKILL.md +4 -3
- package/skills/reviewer-gates/SKILL.md +26 -8
- package/skills/session-log/SKILL.md +11 -12
- package/skills/session-tidy/SKILL.md +40 -30
- package/skills/skill-builder/CLAUDE.md +13 -10
- package/skills/skill-builder/SKILL.md +31 -31
- package/skills/skill-builder/references/CLAUDE.md +10 -6
- package/skills/skill-builder/references/delegation-map.md +13 -4
- package/skills/skill-builder/references/description-field.md +113 -0
- package/skills/skill-builder/references/self-audit-checklist.md +21 -4
- package/skills/skill-builder/references/skill-modularity.md +113 -0
- package/skills/skill-builder/references/skill-types.md +4 -0
- package/skills/skill-builder/templates/CLAUDE.md +4 -4
- package/skills/skill-builder/templates/gap-analysis.md +25 -1
- package/skills/skill-builder/workflows/CLAUDE.md +6 -4
- package/skills/skill-builder/workflows/improve-skill.md +17 -16
- package/skills/skill-builder/workflows/new-skill.md +34 -10
- package/skills/skill-builder/workflows/polish-skill.md +59 -43
- package/skills/team-advisor/SKILL.md +15 -11
- package/skills/usage-pause/SKILL.md +10 -14
- 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
- package/skills/bdd-protocol/CLAUDE.md +0 -26
- package/skills/bdd-protocol/SKILL.md +0 -30
- package/skills/bdd-protocol/references/CLAUDE.md +0 -10
- package/skills/bdd-protocol/references/anti-patterns.md +0 -26
- package/skills/bdd-protocol/references/example-mapping.md +0 -23
- package/skills/bg-agent/CLAUDE.md +0 -17
- package/skills/bg-agent/SKILL.md +0 -69
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
- package/skills/caveman/CLAUDE.md +0 -15
- package/skills/caveman/SKILL.md +0 -38
- package/skills/code/CLAUDE.md +0 -17
- package/skills/code/SKILL.md +0 -178
- package/skills/deep-research/CLAUDE.md +0 -17
- package/skills/deep-research/SKILL.md +0 -80
- package/skills/findbugs/CLAUDE.md +0 -20
- package/skills/findbugs/SKILL.md +0 -241
- package/skills/fixbugs/CLAUDE.md +0 -19
- package/skills/fixbugs/SKILL.md +0 -142
- package/skills/gh-paginate/CLAUDE.md +0 -18
- package/skills/gh-paginate/SKILL.md +0 -84
- package/skills/gotcha/CLAUDE.md +0 -33
- package/skills/gotcha/SKILL.md +0 -73
- package/skills/implement/CLAUDE.md +0 -27
- package/skills/implement/SKILL.md +0 -66
- package/skills/implement/scripts/CLAUDE.md +0 -22
- package/skills/implement/scripts/append_note.py +0 -133
- package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
- package/skills/implement/scripts/test_append_note.py +0 -191
- package/skills/log-audit/CLAUDE.md +0 -20
- package/skills/log-audit/SKILL.md +0 -66
- package/skills/log-audit/reference/CLAUDE.md +0 -9
- package/skills/log-audit/reference/charter.md +0 -52
- package/skills/log-audit/scripts/CLAUDE.md +0 -27
- package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
- package/skills/log-audit/scripts/collect_log_window.py +0 -199
- package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
- package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
- package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
- package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
- package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
- package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
- package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
- package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
- package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
- package/skills/logifix/CLAUDE.md +0 -36
- package/skills/logifix/SKILL.md +0 -69
- package/skills/logifix/scripts/CLAUDE.md +0 -16
- package/skills/logifix/scripts/logifix.ps1 +0 -205
- package/skills/monitor-open-prs/CLAUDE.md +0 -34
- package/skills/monitor-open-prs/SKILL.md +0 -88
- package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
- package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
- package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
- package/skills/monitor-open-prs/test_skill_contract.py +0 -27
- package/skills/post-audit-findings/SKILL.md +0 -80
- package/skills/pr-consistency-audit/CLAUDE.md +0 -34
- package/skills/pr-consistency-audit/SKILL.md +0 -116
- package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
- package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
- package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
- package/skills/pr-review-responder/CLAUDE.md +0 -35
- package/skills/pr-review-responder/EXAMPLES.md +0 -590
- package/skills/pr-review-responder/PRINCIPLES.md +0 -533
- package/skills/pr-review-responder/README.md +0 -168
- package/skills/pr-review-responder/SKILL.md +0 -203
- package/skills/pr-review-responder/TESTING.md +0 -350
- package/skills/pr-scope-resolve/SKILL.md +0 -45
- package/skills/pre-compact/CLAUDE.md +0 -24
- package/skills/pre-compact/SKILL.md +0 -134
- package/skills/qbug/CLAUDE.md +0 -40
- package/skills/qbug/SKILL.md +0 -387
- package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
- package/skills/refine/CLAUDE.md +0 -44
- package/skills/refine/SKILL.md +0 -262
- package/skills/refine/templates/CLAUDE.md +0 -17
- package/skills/refine/templates/implementation-notes-template.html +0 -56
- package/skills/refine/templates/plan-template.md +0 -60
- package/skills/research-mode/CLAUDE.md +0 -35
- package/skills/research-mode/SKILL.md +0 -53
- package/skills/structure-prompt/CLAUDE.md +0 -42
- package/skills/structure-prompt/SKILL.md +0 -44
- package/skills/structure-prompt/reference/CLAUDE.md +0 -28
- package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
- package/skills/structure-prompt/reference/block-classification.md +0 -27
- package/skills/structure-prompt/reference/canonical-case.md +0 -48
- package/skills/structure-prompt/reference/citation-depth.md +0 -70
- package/skills/structure-prompt/reference/cleanup.md +0 -33
- package/skills/structure-prompt/reference/constraints.md +0 -33
- package/skills/structure-prompt/reference/directives.md +0 -37
- package/skills/structure-prompt/reference/examples.md +0 -72
- package/skills/structure-prompt/reference/instantiation.md +0 -51
- package/skills/structure-prompt/reference/output-contract.md +0 -72
- package/skills/structure-prompt/reference/per-category.md +0 -23
- package/skills/structure-prompt/reference/persona.md +0 -38
- package/skills/structure-prompt/reference/research.md +0 -33
- package/skills/structure-prompt/reference/structure.md +0 -28
- package/skills/verified-build/CLAUDE.md +0 -33
- package/skills/verified-build/SKILL.md +0 -33
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tests for the session-scratchpad exemption wired into tdd_enforcer.main."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
TDD_ENFORCER_PATH = Path(__file__).parent / "tdd_enforcer.py"
|
|
15
|
+
FIXED_USER_ID = 5150
|
|
16
|
+
WORKING_DIRECTORY = "/home/user/project"
|
|
17
|
+
SESSION_ID = "tdd-session-987"
|
|
18
|
+
PRODUCTION_CONTENT = "def fulfill_order():\n return True\n"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_tdd_enforcer() -> ModuleType:
|
|
22
|
+
module_spec = importlib.util.spec_from_file_location(
|
|
23
|
+
"tdd_enforcer_scratchpad_under_test", TDD_ENFORCER_PATH
|
|
24
|
+
)
|
|
25
|
+
assert module_spec is not None and module_spec.loader is not None
|
|
26
|
+
loaded_module = importlib.util.module_from_spec(module_spec)
|
|
27
|
+
module_spec.loader.exec_module(loaded_module)
|
|
28
|
+
return loaded_module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_TDD_ENFORCER = _load_tdd_enforcer()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _install_scratchpad_signals(monkeypatch: pytest.MonkeyPatch, temporary_root: Path) -> Path:
|
|
35
|
+
monkeypatch.setattr(os, "getuid", lambda: FIXED_USER_ID, raising=False)
|
|
36
|
+
monkeypatch.setattr(tempfile, "gettempdir", lambda: str(temporary_root))
|
|
37
|
+
mangled_working_directory = WORKING_DIRECTORY.replace("/", "-")
|
|
38
|
+
scratchpad_directory = (
|
|
39
|
+
temporary_root
|
|
40
|
+
/ f"claude-{FIXED_USER_ID}"
|
|
41
|
+
/ mangled_working_directory
|
|
42
|
+
/ SESSION_ID
|
|
43
|
+
/ "scratchpad"
|
|
44
|
+
)
|
|
45
|
+
scratchpad_directory.mkdir(parents=True)
|
|
46
|
+
return scratchpad_directory
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _write_payload(target: Path) -> dict[str, object]:
|
|
50
|
+
return {
|
|
51
|
+
"tool_name": "Write",
|
|
52
|
+
"cwd": WORKING_DIRECTORY,
|
|
53
|
+
"session_id": SESSION_ID,
|
|
54
|
+
"tool_input": {"file_path": str(target), "content": PRODUCTION_CONTENT},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _run_main(
|
|
59
|
+
monkeypatch: pytest.MonkeyPatch, payload: dict[str, object]
|
|
60
|
+
) -> tuple[int | None, str]:
|
|
61
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
|
|
62
|
+
captured_stdout = io.StringIO()
|
|
63
|
+
monkeypatch.setattr(sys, "stdout", captured_stdout)
|
|
64
|
+
exit_code: int | None = None
|
|
65
|
+
try:
|
|
66
|
+
_TDD_ENFORCER.main()
|
|
67
|
+
except SystemExit as raised_exit:
|
|
68
|
+
raw_code = raised_exit.code
|
|
69
|
+
exit_code = raw_code if isinstance(raw_code, int) else None
|
|
70
|
+
return exit_code, captured_stdout.getvalue()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _decision_from(stdout_text: str) -> str | None:
|
|
74
|
+
if not stdout_text.strip():
|
|
75
|
+
return None
|
|
76
|
+
parsed = json.loads(stdout_text)
|
|
77
|
+
return parsed.get("hookSpecificOutput", {}).get("permissionDecision")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_scratchpad_production_write_is_exempt_from_tdd_gate(
|
|
81
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
82
|
+
) -> None:
|
|
83
|
+
scratchpad_directory = _install_scratchpad_signals(monkeypatch, tmp_path)
|
|
84
|
+
monkeypatch.setenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", "1")
|
|
85
|
+
throwaway_script = scratchpad_directory / "one_off_probe.py"
|
|
86
|
+
|
|
87
|
+
exit_code, stdout_text = _run_main(monkeypatch, _write_payload(throwaway_script))
|
|
88
|
+
|
|
89
|
+
assert exit_code == 0
|
|
90
|
+
assert _decision_from(stdout_text) != "deny"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_identical_non_scratchpad_write_is_still_blocked(
|
|
94
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
95
|
+
) -> None:
|
|
96
|
+
_install_scratchpad_signals(monkeypatch, tmp_path)
|
|
97
|
+
monkeypatch.setenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", "1")
|
|
98
|
+
outside_directory = tmp_path / "project" / "orders"
|
|
99
|
+
outside_directory.mkdir(parents=True)
|
|
100
|
+
production_module = outside_directory / "one_off_probe.py"
|
|
101
|
+
|
|
102
|
+
exit_code, stdout_text = _run_main(monkeypatch, _write_payload(production_module))
|
|
103
|
+
|
|
104
|
+
assert exit_code == 0
|
|
105
|
+
assert _decision_from(stdout_text) == "deny"
|
|
@@ -16,8 +16,18 @@ import subprocess
|
|
|
16
16
|
import sys
|
|
17
17
|
|
|
18
18
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
19
|
+
_HOOKS_DIR = _HOOK_DIR.parent
|
|
19
20
|
if str(_HOOK_DIR) not in sys.path:
|
|
20
21
|
sys.path.insert(0, str(_HOOK_DIR))
|
|
22
|
+
if str(_HOOKS_DIR) not in sys.path:
|
|
23
|
+
sys.path.insert(0, str(_HOOKS_DIR))
|
|
24
|
+
|
|
25
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
|
|
26
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
27
|
+
ALL_BASH_HOSTED_HOOK_ENTRIES,
|
|
28
|
+
BASH_TOOL_NAME,
|
|
29
|
+
POWERSHELL_TOOL_NAME,
|
|
30
|
+
)
|
|
21
31
|
|
|
22
32
|
guard_spec = importlib.util.spec_from_file_location(
|
|
23
33
|
"verdict_directory_write_blocker",
|
|
@@ -708,18 +718,43 @@ def _pretooluse_commands_for_matcher(matcher_substring: str) -> list[str]:
|
|
|
708
718
|
return matching_commands
|
|
709
719
|
|
|
710
720
|
|
|
711
|
-
|
|
721
|
+
_VERDICT_GUARD_SCRIPT_RELATIVE_PATH = "blocking/verdict_directory_write_blocker.py"
|
|
722
|
+
_BASH_DISPATCHER_BASENAME = "bash_pre_tool_use_dispatcher.py"
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _verdict_guard_hosted_entries() -> list:
|
|
726
|
+
return [
|
|
727
|
+
each_entry
|
|
728
|
+
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
|
|
729
|
+
if each_entry.script_relative_path == _VERDICT_GUARD_SCRIPT_RELATIVE_PATH
|
|
730
|
+
]
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _assert_bash_dispatcher_registered_for_matcher(matcher_substring: str) -> None:
|
|
734
|
+
matching_commands = _pretooluse_commands_for_matcher(matcher_substring)
|
|
712
735
|
assert any(
|
|
713
|
-
|
|
714
|
-
for each_command in
|
|
736
|
+
_BASH_DISPATCHER_BASENAME in each_command
|
|
737
|
+
for each_command in matching_commands
|
|
738
|
+
), (
|
|
739
|
+
f"bash_pre_tool_use_dispatcher.py must be registered on matcher containing "
|
|
740
|
+
f"{matcher_substring!r}; commands={matching_commands!r}"
|
|
715
741
|
)
|
|
716
742
|
|
|
717
743
|
|
|
744
|
+
def test_guard_is_registered_on_bash() -> None:
|
|
745
|
+
_assert_bash_dispatcher_registered_for_matcher("Bash")
|
|
746
|
+
matching_entries = _verdict_guard_hosted_entries()
|
|
747
|
+
assert len(matching_entries) == 1
|
|
748
|
+
assert matching_entries[0].applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
749
|
+
assert BASH_TOOL_NAME in matching_entries[0].applicable_tool_names
|
|
750
|
+
|
|
751
|
+
|
|
718
752
|
def test_guard_is_registered_on_powershell() -> None:
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
753
|
+
_assert_bash_dispatcher_registered_for_matcher("PowerShell")
|
|
754
|
+
matching_entries = _verdict_guard_hosted_entries()
|
|
755
|
+
assert len(matching_entries) == 1
|
|
756
|
+
assert matching_entries[0].applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
757
|
+
assert POWERSHELL_TOOL_NAME in matching_entries[0].applicable_tool_names
|
|
723
758
|
|
|
724
759
|
|
|
725
760
|
def test_hook_subprocess_imports_real_config_when_parent_holds_shadowing_config(
|
|
@@ -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
|
|
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), plus the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`); 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
|
|
90
|
+
- `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` to resolve private NAS values (with committed placeholder defaults) and the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`).
|
|
89
91
|
|
|
@@ -22,8 +22,6 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
|
|
|
22
22
|
"pr_converge_skill_constants",
|
|
23
23
|
"pr_converge_scripts_constants",
|
|
24
24
|
"bugteam_scripts_constants",
|
|
25
|
-
"implement_scripts_constants",
|
|
26
|
-
"log_audit_constants",
|
|
27
25
|
"usage_pause_constants",
|
|
28
26
|
"dev_env_scripts_constants",
|
|
29
27
|
}
|
|
@@ -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"
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
"""Resolve
|
|
1
|
+
"""Resolve local identity values for NAS ssh and PII exempt-repo hooks.
|
|
2
2
|
|
|
3
|
-
The enforcer
|
|
4
|
-
|
|
5
|
-
Claude home directory
|
|
6
|
-
values
|
|
3
|
+
The enforcer hooks ship into ``~/.claude/`` and cannot read a repository file,
|
|
4
|
+
so they read real machine values from the environment or a git-ignored file in
|
|
5
|
+
the Claude home directory. This module supplies NAS host/user/port, the two
|
|
6
|
+
NAS deny messages that quote those values, and the owner/repo slug set used by
|
|
7
|
+
``pii_exempt_repository_slugs`` (``CLAUDE_PII_EXEMPT_REPOS`` /
|
|
8
|
+
``pii_exempt_repositories``). The committed NAS defaults are placeholders.
|
|
7
9
|
|
|
8
10
|
::
|
|
9
11
|
|
|
10
|
-
CLAUDE_NAS_HOST set to a host
|
|
11
|
-
CLAUDE_NAS_SSH_PORT set to 2200
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
CLAUDE_NAS_HOST set to a host -> nas_host() returns that host
|
|
13
|
+
CLAUDE_NAS_SSH_PORT set to 2200 -> nas_ssh_port() returns 2200
|
|
14
|
+
CLAUDE_PII_EXEMPT_REPOS="Owner/repo" -> {"owner/repo"}
|
|
15
|
+
(env unset, no file) -> nas_host() == "nas.example.local"
|
|
16
|
+
nas_ssh_port() == 22
|
|
17
|
+
pii_exempt_repository_slugs() == frozenset()
|
|
14
18
|
|
|
15
|
-
Each value comes from its
|
|
16
|
-
``~/.claude/local-identity.json``, then the placeholder default.
|
|
19
|
+
Each value comes from its environment variable, then
|
|
20
|
+
``~/.claude/local-identity.json``, then the placeholder default (NAS only).
|
|
17
21
|
"""
|
|
18
22
|
|
|
19
23
|
from __future__ import annotations
|
|
@@ -23,6 +27,9 @@ import os
|
|
|
23
27
|
from pathlib import Path
|
|
24
28
|
|
|
25
29
|
NAS_HOST_ENV_VAR = "CLAUDE_NAS_HOST"
|
|
30
|
+
PII_EXEMPT_REPOS_ENV_VAR = "CLAUDE_PII_EXEMPT_REPOS"
|
|
31
|
+
PII_EXEMPT_REPOS_JSON_KEY = "pii_exempt_repositories"
|
|
32
|
+
PII_EXEMPT_REPOS_SEPARATOR = ","
|
|
26
33
|
NAS_SSH_USER_ENV_VAR = "CLAUDE_NAS_SSH_USER"
|
|
27
34
|
NAS_SSH_PORT_ENV_VAR = "CLAUDE_NAS_SSH_PORT"
|
|
28
35
|
NAS_JSON_KEY = "nas"
|
|
@@ -42,7 +49,7 @@ def _local_identity_file_path() -> Path:
|
|
|
42
49
|
return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / LOCAL_IDENTITY_FILE_NAME
|
|
43
50
|
|
|
44
51
|
|
|
45
|
-
def
|
|
52
|
+
def _identity_dictionary_from_local_file() -> dict[str, object]:
|
|
46
53
|
identity_path = _local_identity_file_path()
|
|
47
54
|
if not identity_path.is_file():
|
|
48
55
|
return {}
|
|
@@ -50,12 +57,55 @@ def _nas_section_from_local_file() -> dict[str, object]:
|
|
|
50
57
|
parsed_identity = json.loads(identity_path.read_text(encoding="utf-8"))
|
|
51
58
|
except (OSError, ValueError, UnicodeDecodeError):
|
|
52
59
|
return {}
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
return parsed_identity if isinstance(parsed_identity, dict) else {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _nas_section_from_local_file() -> dict[str, object]:
|
|
64
|
+
nas_section = _identity_dictionary_from_local_file().get(NAS_JSON_KEY, {})
|
|
56
65
|
return nas_section if isinstance(nas_section, dict) else {}
|
|
57
66
|
|
|
58
67
|
|
|
68
|
+
def _normalized_slug_set(all_raw_slugs: list[object]) -> frozenset[str]:
|
|
69
|
+
return frozenset(
|
|
70
|
+
each_slug.strip().lower()
|
|
71
|
+
for each_slug in all_raw_slugs
|
|
72
|
+
if isinstance(each_slug, str) and each_slug.strip()
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def pii_exempt_repository_slugs() -> frozenset[str]:
|
|
77
|
+
"""Return owner/repo slugs whose commits skip the staged PII scan.
|
|
78
|
+
|
|
79
|
+
::
|
|
80
|
+
|
|
81
|
+
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
|
+
(env unset, file lists ["Owner/repo-a"]) -> {"owner/repo-a"}
|
|
84
|
+
(env unset, no file) -> frozenset()
|
|
85
|
+
|
|
86
|
+
A non-empty environment variable (comma-separated) wins. When the variable
|
|
87
|
+
is unset or normalizes to no slugs, the ``pii_exempt_repositories`` list in
|
|
88
|
+
the git-ignored ``~/.claude/local-identity.json`` is used. Slugs are
|
|
89
|
+
lowercased so matching is case-insensitive.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Lowercased ``owner/repo`` slugs exempt from staged-commit PII scanning.
|
|
93
|
+
"""
|
|
94
|
+
slugs_from_environment = os.environ.get(PII_EXEMPT_REPOS_ENV_VAR)
|
|
95
|
+
if slugs_from_environment is not None:
|
|
96
|
+
normalized_environment_slugs = _normalized_slug_set(
|
|
97
|
+
list(slugs_from_environment.split(PII_EXEMPT_REPOS_SEPARATOR))
|
|
98
|
+
)
|
|
99
|
+
if normalized_environment_slugs:
|
|
100
|
+
return normalized_environment_slugs
|
|
101
|
+
stored_slugs = _identity_dictionary_from_local_file().get(
|
|
102
|
+
PII_EXEMPT_REPOS_JSON_KEY
|
|
103
|
+
)
|
|
104
|
+
if not isinstance(stored_slugs, list):
|
|
105
|
+
return frozenset()
|
|
106
|
+
return _normalized_slug_set(stored_slugs)
|
|
107
|
+
|
|
108
|
+
|
|
59
109
|
def nas_host() -> str:
|
|
60
110
|
"""Return the NAS host the ssh enforcer guards.
|
|
61
111
|
|
|
@@ -187,6 +187,22 @@ ALL_STAGED_FILES_COMMAND: tuple[str, ...] = (
|
|
|
187
187
|
ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX: tuple[str, ...] = ("git", "show")
|
|
188
188
|
STAGED_BLOB_PREFIX: str = ":"
|
|
189
189
|
|
|
190
|
+
ALL_GIT_ORIGIN_URL_COMMAND: tuple[str, ...] = (
|
|
191
|
+
"git",
|
|
192
|
+
"config",
|
|
193
|
+
"--get",
|
|
194
|
+
"remote.origin.url",
|
|
195
|
+
)
|
|
196
|
+
GIT_URL_SUFFIX: str = ".git"
|
|
197
|
+
GITHUB_COM_HOST: str = "github.com"
|
|
198
|
+
ALL_NETWORK_GIT_URL_SCHEMES: frozenset[str] = frozenset({"http", "https", "ssh"})
|
|
199
|
+
SCP_STYLE_PATH_SEPARATOR: str = ":"
|
|
200
|
+
USERINFO_HOST_SEPARATOR: str = "@"
|
|
201
|
+
URL_SCHEME_SEPARATOR: str = "://"
|
|
202
|
+
WINDOWS_PATH_SEPARATOR: str = "\\"
|
|
203
|
+
POSIX_PATH_SEPARATOR: str = "/"
|
|
204
|
+
MINIMUM_OWNER_REPO_SEGMENT_COUNT: int = 2
|
|
205
|
+
|
|
190
206
|
BODY_FILE_ENCODING: str = "utf-8"
|
|
191
207
|
NULL_BYTE_MARKER: bytes = b"\x00"
|
|
192
208
|
MESSAGE_LINE_SEPARATOR: str = "\n"
|
|
@@ -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."
|
|
@@ -121,6 +121,7 @@ DEFAULT_READABILITY_THRESHOLDS: ReadabilityThresholds = ReadabilityThresholds(
|
|
|
121
121
|
__all__ = [
|
|
122
122
|
"ALL_HEAVY_OPENING_HEADERS",
|
|
123
123
|
"ALL_HEAVY_TESTING_HEADERS",
|
|
124
|
+
"ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
|
|
124
125
|
"ALL_READABILITY_CLI_FLAG_TOKENS",
|
|
125
126
|
"ATOMIC_WRITE_TEMP_SUFFIX",
|
|
126
127
|
"BLOCKQUOTE_LINE_PATTERN",
|
|
@@ -135,7 +136,6 @@ __all__ = [
|
|
|
135
136
|
"FLESCH_SYLLABLES_PER_WORD_COEFFICIENT",
|
|
136
137
|
"FLESCH_WORDS_PER_SENTENCE_COEFFICIENT",
|
|
137
138
|
"GH_PR_COMMAND_MIN_TOKEN_COUNT",
|
|
138
|
-
"ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
|
|
139
139
|
"HEADING_LINE_PATTERN",
|
|
140
140
|
"HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION",
|
|
141
141
|
"HEAVY_SHAPE",
|
|
@@ -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
|
+
)
|
|
@@ -66,6 +66,74 @@ class TestNasValuesPlaceholderDefault:
|
|
|
66
66
|
assert local_identity.nas_ssh_port() == 22
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
class TestPiiExemptRepositorySlugs:
|
|
70
|
+
def should_read_slugs_from_the_environment_lowercased(self) -> None:
|
|
71
|
+
with patch.dict(
|
|
72
|
+
os.environ,
|
|
73
|
+
{"CLAUDE_PII_EXEMPT_REPOS": "ExemptOwner/exempt-repo, Other/Repo"},
|
|
74
|
+
clear=False,
|
|
75
|
+
):
|
|
76
|
+
slugs = local_identity.pii_exempt_repository_slugs()
|
|
77
|
+
assert slugs == frozenset({"exemptowner/exempt-repo", "other/repo"})
|
|
78
|
+
|
|
79
|
+
def should_read_slugs_from_the_local_identity_file(self, tmp_path: Path) -> None:
|
|
80
|
+
claude_home = tmp_path / ".claude"
|
|
81
|
+
claude_home.mkdir()
|
|
82
|
+
(claude_home / "local-identity.json").write_text(
|
|
83
|
+
json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
|
|
84
|
+
encoding="utf-8",
|
|
85
|
+
)
|
|
86
|
+
with patch.dict(os.environ, {}, clear=False):
|
|
87
|
+
os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
|
|
88
|
+
with patch.object(Path, "home", return_value=tmp_path):
|
|
89
|
+
slugs = local_identity.pii_exempt_repository_slugs()
|
|
90
|
+
assert slugs == frozenset({"fileowner/file-repo"})
|
|
91
|
+
|
|
92
|
+
def should_return_an_empty_set_when_no_env_and_no_file(
|
|
93
|
+
self, tmp_path: Path
|
|
94
|
+
) -> None:
|
|
95
|
+
with patch.dict(os.environ, {}, clear=False):
|
|
96
|
+
os.environ.pop("CLAUDE_PII_EXEMPT_REPOS", None)
|
|
97
|
+
with patch.object(Path, "home", return_value=tmp_path):
|
|
98
|
+
assert local_identity.pii_exempt_repository_slugs() == frozenset()
|
|
99
|
+
|
|
100
|
+
def should_prefer_environment_slugs_over_local_identity_file(
|
|
101
|
+
self, tmp_path: Path
|
|
102
|
+
) -> None:
|
|
103
|
+
claude_home = tmp_path / ".claude"
|
|
104
|
+
claude_home.mkdir()
|
|
105
|
+
(claude_home / "local-identity.json").write_text(
|
|
106
|
+
json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
|
|
107
|
+
encoding="utf-8",
|
|
108
|
+
)
|
|
109
|
+
with patch.dict(
|
|
110
|
+
os.environ,
|
|
111
|
+
{"CLAUDE_PII_EXEMPT_REPOS": "EnvOwner/env-repo"},
|
|
112
|
+
clear=False,
|
|
113
|
+
):
|
|
114
|
+
with patch.object(Path, "home", return_value=tmp_path):
|
|
115
|
+
slugs = local_identity.pii_exempt_repository_slugs()
|
|
116
|
+
assert slugs == frozenset({"envowner/env-repo"})
|
|
117
|
+
|
|
118
|
+
def should_fall_through_to_json_when_environment_is_whitespace_only(
|
|
119
|
+
self, tmp_path: Path
|
|
120
|
+
) -> None:
|
|
121
|
+
claude_home = tmp_path / ".claude"
|
|
122
|
+
claude_home.mkdir()
|
|
123
|
+
(claude_home / "local-identity.json").write_text(
|
|
124
|
+
json.dumps({"pii_exempt_repositories": ["FileOwner/file-repo"]}),
|
|
125
|
+
encoding="utf-8",
|
|
126
|
+
)
|
|
127
|
+
with patch.dict(
|
|
128
|
+
os.environ,
|
|
129
|
+
{"CLAUDE_PII_EXEMPT_REPOS": " , , "},
|
|
130
|
+
clear=False,
|
|
131
|
+
):
|
|
132
|
+
with patch.object(Path, "home", return_value=tmp_path):
|
|
133
|
+
slugs = local_identity.pii_exempt_repository_slugs()
|
|
134
|
+
assert slugs == frozenset({"fileowner/file-repo"})
|
|
135
|
+
|
|
136
|
+
|
|
69
137
|
class TestDenyMessagesQuoteTheResolvedHost:
|
|
70
138
|
def should_include_the_resolved_host_and_port_in_the_bare_binary_message(
|
|
71
139
|
self,
|
package/hooks/pyproject.toml
CHANGED
|
@@ -9,10 +9,24 @@
|
|
|
9
9
|
mypy_path = "."
|
|
10
10
|
python_version = "3.13"
|
|
11
11
|
ignore_missing_imports = true
|
|
12
|
-
warn_unused_ignores =
|
|
12
|
+
warn_unused_ignores = false
|
|
13
13
|
warn_redundant_casts = true
|
|
14
14
|
no_implicit_optional = true
|
|
15
15
|
strict_equality = true
|
|
16
|
+
exclude = [
|
|
17
|
+
"validators/test_files/",
|
|
18
|
+
"test_.*\\.py$",
|
|
19
|
+
".*/test_.*\\.py$",
|
|
20
|
+
# One-line mypy fixes re-surface whole-file CODE_RULES debt at pre-commit (#51).
|
|
21
|
+
"abbreviation_checks\\.py$",
|
|
22
|
+
"git_checks\\.py$",
|
|
23
|
+
"exempt_paths\\.py$",
|
|
24
|
+
"code_rules_typeddict_stub\\.py$",
|
|
25
|
+
"validators/exempt_paths\\.py$",
|
|
26
|
+
"validators/abbreviation_checks\\.py$",
|
|
27
|
+
"validators/git_checks\\.py$",
|
|
28
|
+
"blocking/code_rules_typeddict_stub\\.py$",
|
|
29
|
+
]
|
|
16
30
|
|
|
17
31
|
[[tool.mypy.overrides]]
|
|
18
32
|
module = "tests.*"
|
|
@@ -26,6 +40,7 @@ extend-exclude = [
|
|
|
26
40
|
".claude",
|
|
27
41
|
"node_modules",
|
|
28
42
|
"__pycache__",
|
|
43
|
+
"validators/test_files",
|
|
29
44
|
]
|
|
30
45
|
|
|
31
46
|
[tool.ruff.lint]
|
|
@@ -39,16 +54,72 @@ select = [
|
|
|
39
54
|
"SIM",
|
|
40
55
|
"PL",
|
|
41
56
|
]
|
|
57
|
+
# Gate-restore (#51): principled + pure-style ignores. Non-negotiable defect
|
|
58
|
+
# classes stay selected; residual hits live under per-file-ignores where a
|
|
59
|
+
# one-line fix is blocked by whole-file CODE_RULES pre-commit debt.
|
|
42
60
|
ignore = [
|
|
43
61
|
"E501",
|
|
62
|
+
"E402",
|
|
63
|
+
"UP038",
|
|
64
|
+
"UP006",
|
|
65
|
+
"UP007",
|
|
66
|
+
"UP015",
|
|
67
|
+
"UP017",
|
|
68
|
+
"UP020",
|
|
69
|
+
"UP024",
|
|
70
|
+
"UP035",
|
|
71
|
+
"UP045",
|
|
72
|
+
"I001",
|
|
73
|
+
"B009",
|
|
74
|
+
"F541",
|
|
75
|
+
"PLR0911",
|
|
76
|
+
"PLR0912",
|
|
44
77
|
"PLR0913",
|
|
78
|
+
"PLR0915",
|
|
79
|
+
"PLR1714",
|
|
45
80
|
"PLR2004",
|
|
81
|
+
"PLR5501",
|
|
82
|
+
"PLC0414",
|
|
83
|
+
"PLC0415",
|
|
84
|
+
"SIM101",
|
|
85
|
+
"SIM102",
|
|
86
|
+
"SIM103",
|
|
87
|
+
"SIM105",
|
|
88
|
+
"SIM109",
|
|
89
|
+
"SIM110",
|
|
90
|
+
"SIM113",
|
|
91
|
+
"SIM114",
|
|
92
|
+
"SIM117",
|
|
93
|
+
"SIM300",
|
|
46
94
|
]
|
|
47
95
|
|
|
48
96
|
[tool.ruff.lint.per-file-ignores]
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
97
|
+
"advisory/refactor_guard.py" = ["F401", "PLW1510"]
|
|
98
|
+
"blocking/block_main_commit.py" = ["PLW1510"]
|
|
99
|
+
"blocking/code_rules_boolean_mustcheck.py" = ["B905"]
|
|
100
|
+
"blocking/code_rules_mock_completeness.py" = ["B007"]
|
|
101
|
+
"blocking/code_rules_optional_params.py" = ["B905"]
|
|
102
|
+
"blocking/code_rules_shared.py" = ["B905"]
|
|
103
|
+
"blocking/code_rules_test_assertions.py" = ["B905"]
|
|
104
|
+
"blocking/code_rules_typeddict_stub.py" = ["B905"]
|
|
105
|
+
"blocking/pii_prevention_blocker.py" = ["PLW1510", "B018"]
|
|
106
|
+
"blocking/pr_description_command_parser.py" = ["B904"]
|
|
107
|
+
"blocking/precommit_code_rules_gate.py" = ["PLW1510"]
|
|
108
|
+
"diagnostic/hook_log_extractor.py" = ["SIM115"]
|
|
109
|
+
"git-hooks/post_commit.py" = ["F401", "PLW1510"]
|
|
110
|
+
"hooks_constants/banned_identifiers_constants.py" = ["B033"]
|
|
111
|
+
"session/plugin_data_dir_cleanup.py" = ["F401"]
|
|
112
|
+
"validation/mypy_validator.py" = ["PLW1510"]
|
|
113
|
+
"validators/exempt_paths.py" = ["F401"]
|
|
114
|
+
"validators/health_check.py" = ["PLW1510"]
|
|
115
|
+
"validators/mypy_integration.py" = ["PLW1510"]
|
|
116
|
+
"validators/python_style_checks.py" = ["B905"]
|
|
117
|
+
"validators/ruff_integration.py" = ["PLW1510"]
|
|
118
|
+
"validators/run_all_validators.py" = ["PLW1510"]
|
|
119
|
+
"workflow/auto_formatter.py" = ["PLW1510"]
|
|
120
|
+
"test_*.py" = ["B011", "PLR2004", "F401", "F841", "PLW1510", "B905", "B033", "SIM115", "B007", "B018", "B904"]
|
|
121
|
+
"*_test.py" = ["B011", "PLR2004", "F401", "F841", "PLW1510", "B905", "B033", "SIM115", "B007", "B018", "B904"]
|
|
122
|
+
"conftest.py" = ["B011", "F401", "F841", "PLW1510"]
|
|
52
123
|
|
|
53
124
|
[tool.coverage.run]
|
|
54
125
|
branch = true
|
|
@@ -12,6 +12,7 @@ A library of check modules used by the validation hooks. Each module focuses on
|
|
|
12
12
|
| `output_formatter.py` | Formats `Violation` lists into human-readable output |
|
|
13
13
|
| `run_all_validators.py` | Entry point — runs every check module and aggregates results |
|
|
14
14
|
| `health_check.py` | Verifies that all validator dependencies (ruff, mypy) are reachable |
|
|
15
|
+
| `python_style_helpers.py` | Shared source-line splitting and function-discovery helpers imported by `python_style_checks.py` |
|
|
15
16
|
|
|
16
17
|
## Check modules
|
|
17
18
|
|
|
@@ -33,7 +34,6 @@ A library of check modules used by the validation hooks. Each module focuses on
|
|
|
33
34
|
| `todo_checks.py` | TODO/FIXME markers without an associated issue reference |
|
|
34
35
|
| `type_safety_checks.py` | Type-safety rules (no `Any`, no `cast`, no `# type: ignore`) |
|
|
35
36
|
| `useless_test_checks.py` | Tests that check only existence or constant values |
|
|
36
|
-
| `verify_paths.py` | Validates that file paths referenced in code actually exist |
|
|
37
37
|
|
|
38
38
|
## Subdirectory
|
|
39
39
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
AST-based Python style checks for code quality enforcement.
|
|
4
4
|
|
|
5
|
+
The checks live in `python_style_checks.py`; the shared source-line splitting and function-discovery helpers they build on live in `python_style_helpers.py`.
|
|
6
|
+
|
|
5
7
|
## Checks Implemented
|
|
6
8
|
|
|
7
9
|
1. **Imports at top** - All import statements must be at the top of the file
|