claude-dev-env 1.93.1 → 1.94.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/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 +1 -1
- package/bin/install.mjs +2 -7
- package/bin/install.test.mjs +10 -4
- package/hooks/blocking/CLAUDE.md +2 -0
- 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/test_pii_prevention_blocker.py +299 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
- package/hooks/hooks_constants/local_identity.py +65 -15
- package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
- package/hooks/hooks_constants/test_local_identity.py +68 -0
- package/package.json +1 -1
- 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 +3 -25
- 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 +284 -94
- 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/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/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 +12 -8
- package/skills/orchestrator-refresh/SKILL.md +3 -3
- 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/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/usage-pause/SKILL.md +10 -14
- 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-fix-protocol/SKILL.md +0 -65
- 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
|
@@ -28,6 +28,7 @@ import pii_prevention_blocker as blocker_module # noqa: E402
|
|
|
28
28
|
from pii_prevention_blocker import ( # noqa: E402
|
|
29
29
|
evaluate,
|
|
30
30
|
evaluate_bash_command,
|
|
31
|
+
evaluate_post_body_texts,
|
|
31
32
|
evaluate_staged_commit,
|
|
32
33
|
evaluate_write_edit_payload,
|
|
33
34
|
extract_git_commit_working_directory,
|
|
@@ -903,3 +904,301 @@ def test_git_exe_dash_c_commit_scans_named_repo_not_cwd(tmp_path: Path) -> None:
|
|
|
903
904
|
assert dash_c_scan is not None
|
|
904
905
|
assert "email" in dash_c_scan
|
|
905
906
|
assert SYNTHETIC_REAL_EMAIL not in dash_c_scan
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _add_repository_origin(repository_root: Path, origin_url: str) -> None:
|
|
910
|
+
subprocess.run(
|
|
911
|
+
["git", "remote", "add", "origin", origin_url],
|
|
912
|
+
cwd=repository_root,
|
|
913
|
+
check=True,
|
|
914
|
+
capture_output=True,
|
|
915
|
+
text=True,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def test_exempt_repository_commit_skips_staged_pii_scan(
|
|
920
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
921
|
+
) -> None:
|
|
922
|
+
repository_root = tmp_path / "repo"
|
|
923
|
+
_init_repo_with_staged_email(repository_root)
|
|
924
|
+
_add_repository_origin(
|
|
925
|
+
repository_root, "https://github.com/ExemptOwner/exempt-repo.git"
|
|
926
|
+
)
|
|
927
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
928
|
+
deny_reason = evaluate_bash_command(
|
|
929
|
+
"git commit -m test",
|
|
930
|
+
working_directory=str(repository_root),
|
|
931
|
+
)
|
|
932
|
+
assert deny_reason is None
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def test_exempt_repository_matches_ssh_origin_url(
|
|
936
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
937
|
+
) -> None:
|
|
938
|
+
repository_root = tmp_path / "repo"
|
|
939
|
+
_init_repo_with_staged_email(repository_root)
|
|
940
|
+
_add_repository_origin(
|
|
941
|
+
repository_root, "git@github.com:ExemptOwner/exempt-repo.git"
|
|
942
|
+
)
|
|
943
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "exemptowner/EXEMPT-REPO")
|
|
944
|
+
deny_reason = evaluate_bash_command(
|
|
945
|
+
"git commit -m test",
|
|
946
|
+
working_directory=str(repository_root),
|
|
947
|
+
)
|
|
948
|
+
assert deny_reason is None
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def test_non_exempt_repository_commit_still_scans_staged_pii(
|
|
952
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
953
|
+
) -> None:
|
|
954
|
+
repository_root = tmp_path / "repo"
|
|
955
|
+
_init_repo_with_staged_email(repository_root)
|
|
956
|
+
_add_repository_origin(
|
|
957
|
+
repository_root, "https://github.com/OtherOwner/other-repo.git"
|
|
958
|
+
)
|
|
959
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
960
|
+
deny_reason = evaluate_bash_command(
|
|
961
|
+
"git commit -m test",
|
|
962
|
+
working_directory=str(repository_root),
|
|
963
|
+
)
|
|
964
|
+
assert deny_reason is not None
|
|
965
|
+
assert "email" in deny_reason
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def test_repository_without_origin_is_not_exempt(
|
|
969
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
970
|
+
) -> None:
|
|
971
|
+
repository_root = tmp_path / "repo"
|
|
972
|
+
_init_repo_with_staged_email(repository_root)
|
|
973
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
974
|
+
deny_reason = evaluate_bash_command(
|
|
975
|
+
"git commit -m test",
|
|
976
|
+
working_directory=str(repository_root),
|
|
977
|
+
)
|
|
978
|
+
assert deny_reason is not None
|
|
979
|
+
assert "email" in deny_reason
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def test_owner_repo_slug_from_https_origin_with_git_suffix() -> None:
|
|
983
|
+
assert (
|
|
984
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
985
|
+
"https://github.com/Owner/repo.git"
|
|
986
|
+
)
|
|
987
|
+
== "owner/repo"
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def test_owner_repo_slug_from_https_origin_without_git_suffix() -> None:
|
|
992
|
+
assert (
|
|
993
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
994
|
+
"https://github.com/Owner/repo"
|
|
995
|
+
)
|
|
996
|
+
== "owner/repo"
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def test_owner_repo_slug_from_scp_ssh_origin() -> None:
|
|
1001
|
+
assert (
|
|
1002
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1003
|
+
"git@github.com:Owner/repo.git"
|
|
1004
|
+
)
|
|
1005
|
+
== "owner/repo"
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def test_owner_repo_slug_from_ssh_scheme_origin() -> None:
|
|
1010
|
+
assert (
|
|
1011
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1012
|
+
"ssh://git@github.com/Owner/repo.git"
|
|
1013
|
+
)
|
|
1014
|
+
== "owner/repo"
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def test_owner_repo_slug_from_trailing_slash_git_suffix() -> None:
|
|
1019
|
+
assert (
|
|
1020
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1021
|
+
"https://github.com/Owner/repo.git/"
|
|
1022
|
+
)
|
|
1023
|
+
== "owner/repo"
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def test_owner_repo_slug_rejects_spoofed_evil_host() -> None:
|
|
1028
|
+
assert (
|
|
1029
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1030
|
+
"https://evil.test/ExemptOwner/exempt-repo.git"
|
|
1031
|
+
)
|
|
1032
|
+
is None
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def test_owner_repo_slug_rejects_notgithub_com_host() -> None:
|
|
1037
|
+
assert (
|
|
1038
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1039
|
+
"https://notgithub.com/Owner/repo.git"
|
|
1040
|
+
)
|
|
1041
|
+
is None
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def test_owner_repo_slug_rejects_github_com_suffix_host() -> None:
|
|
1046
|
+
assert (
|
|
1047
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1048
|
+
"https://github.com.evil.test/Owner/repo.git"
|
|
1049
|
+
)
|
|
1050
|
+
is None
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def test_owner_repo_slug_rejects_invalid_https_port_authority() -> None:
|
|
1055
|
+
assert (
|
|
1056
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1057
|
+
"https://github.com:443.evil.test/Owner/repo.git"
|
|
1058
|
+
)
|
|
1059
|
+
is None
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def test_owner_repo_slug_rejects_invalid_ssh_port_authority() -> None:
|
|
1064
|
+
assert (
|
|
1065
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1066
|
+
"ssh://git@github.com:22.evil.test/Owner/repo.git"
|
|
1067
|
+
)
|
|
1068
|
+
is None
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def test_owner_repo_slug_from_https_origin_with_valid_port() -> None:
|
|
1073
|
+
assert (
|
|
1074
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1075
|
+
"https://github.com:443/Owner/repo.git"
|
|
1076
|
+
)
|
|
1077
|
+
== "owner/repo"
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def test_owner_repo_slug_from_ssh_origin_with_valid_port() -> None:
|
|
1082
|
+
assert (
|
|
1083
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1084
|
+
"ssh://git@github.com:22/Owner/repo.git"
|
|
1085
|
+
)
|
|
1086
|
+
== "owner/repo"
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def test_owner_repo_slug_rejects_path_shaped_origin() -> None:
|
|
1091
|
+
assert (
|
|
1092
|
+
blocker_module._owner_repo_slug_from_origin_url(
|
|
1093
|
+
"C:/repos/ExemptOwner/exempt-repo"
|
|
1094
|
+
)
|
|
1095
|
+
is None
|
|
1096
|
+
)
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def test_spoofed_origin_host_still_denies_staged_email(
|
|
1100
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1101
|
+
) -> None:
|
|
1102
|
+
repository_root = tmp_path / "repo"
|
|
1103
|
+
_init_repo_with_staged_email(repository_root)
|
|
1104
|
+
_add_repository_origin(
|
|
1105
|
+
repository_root, "https://evil.test/ExemptOwner/exempt-repo.git"
|
|
1106
|
+
)
|
|
1107
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
1108
|
+
deny_reason = evaluate_bash_command(
|
|
1109
|
+
"git commit -m test",
|
|
1110
|
+
working_directory=str(repository_root),
|
|
1111
|
+
)
|
|
1112
|
+
assert deny_reason is not None
|
|
1113
|
+
assert "email" in deny_reason
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def test_invalid_port_authority_origin_still_denies_staged_email(
|
|
1117
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1118
|
+
) -> None:
|
|
1119
|
+
repository_root = tmp_path / "repo"
|
|
1120
|
+
_init_repo_with_staged_email(repository_root)
|
|
1121
|
+
_add_repository_origin(
|
|
1122
|
+
repository_root,
|
|
1123
|
+
"https://github.com:443.evil.test/ExemptOwner/exempt-repo.git",
|
|
1124
|
+
)
|
|
1125
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
1126
|
+
deny_reason = evaluate_bash_command(
|
|
1127
|
+
"git commit -m test",
|
|
1128
|
+
working_directory=str(repository_root),
|
|
1129
|
+
)
|
|
1130
|
+
assert deny_reason is not None
|
|
1131
|
+
assert "email" in deny_reason
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def test_path_shaped_origin_still_denies_staged_email(
|
|
1135
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1136
|
+
) -> None:
|
|
1137
|
+
repository_root = tmp_path / "repo"
|
|
1138
|
+
_init_repo_with_staged_email(repository_root)
|
|
1139
|
+
_add_repository_origin(repository_root, "C:/repos/ExemptOwner/exempt-repo")
|
|
1140
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
1141
|
+
deny_reason = evaluate_bash_command(
|
|
1142
|
+
"git commit -m test",
|
|
1143
|
+
working_directory=str(repository_root),
|
|
1144
|
+
)
|
|
1145
|
+
assert deny_reason is not None
|
|
1146
|
+
assert "email" in deny_reason
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
def test_trailing_slash_git_suffix_origin_still_exempts(
|
|
1150
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1151
|
+
) -> None:
|
|
1152
|
+
repository_root = tmp_path / "repo"
|
|
1153
|
+
_init_repo_with_staged_email(repository_root)
|
|
1154
|
+
_add_repository_origin(
|
|
1155
|
+
repository_root, "https://github.com/ExemptOwner/exempt-repo.git/"
|
|
1156
|
+
)
|
|
1157
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
1158
|
+
deny_reason = evaluate_bash_command(
|
|
1159
|
+
"git commit -m test",
|
|
1160
|
+
working_directory=str(repository_root),
|
|
1161
|
+
)
|
|
1162
|
+
assert deny_reason is None
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
def test_ssh_scheme_origin_still_exempts(
|
|
1166
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1167
|
+
) -> None:
|
|
1168
|
+
repository_root = tmp_path / "repo"
|
|
1169
|
+
_init_repo_with_staged_email(repository_root)
|
|
1170
|
+
_add_repository_origin(
|
|
1171
|
+
repository_root, "ssh://git@github.com/ExemptOwner/exempt-repo.git"
|
|
1172
|
+
)
|
|
1173
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
1174
|
+
deny_reason = evaluate_bash_command(
|
|
1175
|
+
"git commit -m test",
|
|
1176
|
+
working_directory=str(repository_root),
|
|
1177
|
+
)
|
|
1178
|
+
assert deny_reason is None
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
def test_write_still_scans_when_repository_is_exempt(
|
|
1182
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1183
|
+
) -> None:
|
|
1184
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
1185
|
+
deny_reason = evaluate_write_edit_payload(
|
|
1186
|
+
"Write",
|
|
1187
|
+
{
|
|
1188
|
+
"file_path": "notes.md",
|
|
1189
|
+
"content": f"Reach me at {SYNTHETIC_REAL_EMAIL}",
|
|
1190
|
+
},
|
|
1191
|
+
)
|
|
1192
|
+
assert deny_reason is not None
|
|
1193
|
+
assert "email" in deny_reason
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def test_post_body_still_scans_when_repository_is_exempt(
|
|
1197
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1198
|
+
) -> None:
|
|
1199
|
+
monkeypatch.setenv("CLAUDE_PII_EXEMPT_REPOS", "ExemptOwner/exempt-repo")
|
|
1200
|
+
deny_reason = evaluate_post_body_texts(
|
|
1201
|
+
[f"Reach me at {SYNTHETIC_REAL_EMAIL}"]
|
|
1202
|
+
)
|
|
1203
|
+
assert deny_reason is not None
|
|
1204
|
+
assert "email" in deny_reason
|
|
@@ -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(
|
|
@@ -38,7 +38,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
38
38
|
| `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
39
|
| `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
|
|
40
40
|
| `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
|
|
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), 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
42
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
43
43
|
| `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
44
|
| `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 |
|
|
@@ -85,5 +85,5 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
85
85
|
- Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
|
|
86
86
|
- Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
|
|
87
87
|
- `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
|
|
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) and the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`).
|
|
89
89
|
|
|
@@ -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
|
}
|
|
@@ -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"
|
|
@@ -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",
|
|
@@ -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/package.json
CHANGED
package/scripts/CLAUDE.md
CHANGED
|
@@ -21,6 +21,7 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
|
|
|
21
21
|
| `Install-SweepEmptyDirs.ps1` | Registers `sweep_empty_dirs.py` as a scheduled task on Windows |
|
|
22
22
|
| `check.ps1` | Runs the full code-quality check suite |
|
|
23
23
|
| `Show-Asset.ps1` | Opens files on screen, sizing each image window to the image's pixel dimensions (scaled to fit the screen); non-image files open in their default application |
|
|
24
|
+
| `Get-SessionAccount.ps1` | Reports which Claude account the current session is actually logged into by comparing `~/.claude.json`'s CLI login against a `CLAUDE_USER_DATA_DIR` desktop profile's `lastKnownAccountUuid`, recovering the desktop account's email from profile storage when the two accounts differ |
|
|
24
25
|
|
|
25
26
|
## Subdirectories
|
|
26
27
|
|
|
@@ -28,10 +29,18 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
|
|
|
28
29
|
|---|---|
|
|
29
30
|
| `dev_env_scripts_constants/` | Named constants (`timing.py`) for scripts in this directory |
|
|
30
31
|
| `sync_to_cursor/` | Package that builds Cursor `.mdc` files from Claude rules and docs |
|
|
31
|
-
| `tests/` | pytest suite for the Python scripts in this directory |
|
|
32
|
+
| `tests/` | pytest suite for the Python scripts and Pester (`*.Tests.ps1`) suite for the PowerShell scripts in this directory |
|
|
32
33
|
|
|
33
34
|
## Running tests
|
|
34
35
|
|
|
36
|
+
Python scripts (pytest):
|
|
37
|
+
|
|
35
38
|
```bash
|
|
36
39
|
python -m pytest packages/claude-dev-env/scripts/tests/
|
|
37
40
|
```
|
|
41
|
+
|
|
42
|
+
PowerShell scripts (Pester 5+, `*.Tests.ps1`):
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
Invoke-Pester -Path packages/claude-dev-env/scripts/tests/
|
|
46
|
+
```
|