claude-dev-env 7.0.0 → 7.2.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/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: block writes, durable posts, and commits that carry PII.
|
|
3
|
-
|
|
4
|
-
::
|
|
5
|
-
|
|
6
|
-
Write/Edit/MultiEdit -> scan new disk content
|
|
7
|
-
gh post / GitHub MCP -> scan durable bodies
|
|
8
|
-
git commit -> scan staged blob text, not the commit message
|
|
9
|
-
|
|
10
|
-
``pii_payload_scan`` and ``pii_commit_command`` re-export through this
|
|
11
|
-
entry module so hooks.json wiring and importers stay unchanged.
|
|
12
|
-
``CLAUDE_PII_EXEMPT_REPOS`` or ``pii_exempt_repositories`` in
|
|
13
|
-
``~/.claude/local-identity.json`` skip the staged-commit scan. A repo
|
|
14
|
-
without a readable origin remote is never exempt.
|
|
15
|
-
``pii_allowlisted_values`` for a repository origin slug may pass
|
|
16
|
-
Write/Edit and staged-commit scans in that repository only.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
import subprocess
|
|
23
|
-
import sys
|
|
24
|
-
from pathlib import Path
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
28
|
-
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
29
|
-
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
30
|
-
if each_bootstrap_directory not in sys.path:
|
|
31
|
-
sys.path.insert(0, each_bootstrap_directory)
|
|
32
|
-
from pii_commit_command import (
|
|
33
|
-
extract_git_commit_working_directory,
|
|
34
|
-
is_git_commit_shell_command,
|
|
35
|
-
)
|
|
36
|
-
from pii_payload_scan import (
|
|
37
|
-
build_deny_reason,
|
|
38
|
-
evaluate_apply_patch_payload,
|
|
39
|
-
evaluate_post_body_texts,
|
|
40
|
-
evaluate_write_edit_payload,
|
|
41
|
-
)
|
|
42
|
-
from pii_prevention_blocker_parts.repository_exemption import (
|
|
43
|
-
_is_repository_exempt_from_pii_scan,
|
|
44
|
-
_owner_repo_slug_from_origin_url,
|
|
45
|
-
repository_allowlisted_values,
|
|
46
|
-
)
|
|
47
|
-
from pii_prevention_blocker_parts.repository_resolution import (
|
|
48
|
-
compose_command_working_directory,
|
|
49
|
-
expand_user_directory,
|
|
50
|
-
refusal_reason_for_unresolved_repository,
|
|
51
|
-
)
|
|
52
|
-
from pii_scanner import is_path_exempt_from_pii_scan, scan_text_for_pii
|
|
53
|
-
from precommit_code_rules_gate import resolve_repository_root
|
|
54
|
-
from gh_post_body_texts import (
|
|
55
|
-
extract_gh_post_body_texts_for_privacy_gate,
|
|
56
|
-
extract_mcp_body_texts,
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
from hooks_constants.hook_block_logger import log_hook_block
|
|
60
|
-
from hooks_constants.pii_prevention_constants import (
|
|
61
|
-
ALL_SHELL_TOOL_NAMES,
|
|
62
|
-
ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX,
|
|
63
|
-
ALL_STAGED_FILES_COMMAND,
|
|
64
|
-
ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
65
|
-
APPLY_PATCH_TOOL_NAME,
|
|
66
|
-
BODY_FILE_ENCODING,
|
|
67
|
-
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
68
|
-
HOOK_SCRIPT_BASENAME,
|
|
69
|
-
MAXIMUM_STAGED_FILE_BYTES,
|
|
70
|
-
MCP_GITHUB_TOOL_PREFIX,
|
|
71
|
-
NULL_BYTE_MARKER,
|
|
72
|
-
STAGED_BLOB_PREFIX,
|
|
73
|
-
STAGED_BLOB_REASON_DECODE_FAILED,
|
|
74
|
-
STAGED_BLOB_REASON_GIT_SHOW_FAILED,
|
|
75
|
-
STAGED_BLOB_REASON_NULL_BYTES,
|
|
76
|
-
STAGED_BLOB_REASON_OVERSIZED,
|
|
77
|
-
STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE,
|
|
78
|
-
STAGED_LIST_FAILURE_REASON,
|
|
79
|
-
)
|
|
80
|
-
from hooks_constants.pre_tool_use_stdin import (
|
|
81
|
-
read_hook_input_dictionary_from_stdin,
|
|
82
|
-
)
|
|
83
|
-
except ImportError as import_error:
|
|
84
|
-
raise ImportError(
|
|
85
|
-
"pii_prevention_blocker: cannot import its sibling modules; "
|
|
86
|
-
"ensure the blocking and hooks directories are importable."
|
|
87
|
-
) from import_error
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
__all__ = [
|
|
91
|
-
"evaluate",
|
|
92
|
-
"evaluate_apply_patch_payload",
|
|
93
|
-
"evaluate_bash_command",
|
|
94
|
-
"evaluate_post_body_texts",
|
|
95
|
-
"evaluate_staged_commit",
|
|
96
|
-
"evaluate_write_edit_payload",
|
|
97
|
-
"extract_git_commit_working_directory",
|
|
98
|
-
"is_git_commit_shell_command",
|
|
99
|
-
"list_staged_file_paths",
|
|
100
|
-
"read_staged_file_text",
|
|
101
|
-
"resolve_repository_root",
|
|
102
|
-
"_owner_repo_slug_from_origin_url",
|
|
103
|
-
"main",
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def list_staged_file_paths(
|
|
108
|
-
repository_root: Path,
|
|
109
|
-
) -> tuple[list[str] | None, str | None]:
|
|
110
|
-
"""List staged non-deleted paths, or report a list failure.
|
|
111
|
-
|
|
112
|
-
Args:
|
|
113
|
-
repository_root: Repository root used as the git working directory.
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
``(paths, None)`` on success, or ``(None, deny_reason)`` when the
|
|
117
|
-
staged list cannot be read (fail-closed for commit gating).
|
|
118
|
-
"""
|
|
119
|
-
try:
|
|
120
|
-
completed_process = subprocess.run(
|
|
121
|
-
list(ALL_STAGED_FILES_COMMAND),
|
|
122
|
-
check=False,
|
|
123
|
-
capture_output=True,
|
|
124
|
-
text=True,
|
|
125
|
-
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
126
|
-
cwd=str(repository_root),
|
|
127
|
-
)
|
|
128
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
129
|
-
return None, STAGED_LIST_FAILURE_REASON
|
|
130
|
-
if completed_process.returncode != 0:
|
|
131
|
-
return None, STAGED_LIST_FAILURE_REASON
|
|
132
|
-
all_paths = [
|
|
133
|
-
each_line.strip()
|
|
134
|
-
for each_line in completed_process.stdout.splitlines()
|
|
135
|
-
if each_line.strip()
|
|
136
|
-
]
|
|
137
|
-
return all_paths, None
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _git_show_staged_blob(
|
|
141
|
-
repository_root: Path, staged_blob_reference: str
|
|
142
|
-
) -> subprocess.CompletedProcess[bytes] | None:
|
|
143
|
-
"""Return the ``git show`` result for a staged blob, or None on failure."""
|
|
144
|
-
try:
|
|
145
|
-
completed_process = subprocess.run(
|
|
146
|
-
list(ALL_STAGED_BLOB_SHOW_COMMAND_PREFIX) + [staged_blob_reference],
|
|
147
|
-
check=False,
|
|
148
|
-
capture_output=True,
|
|
149
|
-
timeout=GIT_COMMAND_TIMEOUT_SECONDS,
|
|
150
|
-
cwd=str(repository_root),
|
|
151
|
-
)
|
|
152
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
153
|
-
return None
|
|
154
|
-
if completed_process.returncode != 0:
|
|
155
|
-
return None
|
|
156
|
-
return completed_process
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _unscannable_result(relative_path: str, reason: str) -> tuple[None, str]:
|
|
160
|
-
"""Build the ``(None, deny_reason)`` pair for an unscannable staged blob."""
|
|
161
|
-
return None, STAGED_BLOB_UNSCANNABLE_REASON_TEMPLATE.format(
|
|
162
|
-
relative_path=relative_path, reason=reason
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def read_staged_file_text(
|
|
167
|
-
repository_root: Path, relative_path: str
|
|
168
|
-
) -> tuple[str | None, str | None]:
|
|
169
|
-
"""Return staged blob text, or ``(None, deny_reason)`` when unscannable.
|
|
170
|
-
|
|
171
|
-
Args:
|
|
172
|
-
repository_root: Repository root for the git show working directory.
|
|
173
|
-
relative_path: Repository-relative path of the staged file.
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
``(text, None)`` for scannable UTF-8 text; ``(None, reason)`` otherwise.
|
|
177
|
-
"""
|
|
178
|
-
staged_blob_reference = STAGED_BLOB_PREFIX + relative_path
|
|
179
|
-
completed_process = _git_show_staged_blob(repository_root, staged_blob_reference)
|
|
180
|
-
if completed_process is None:
|
|
181
|
-
return _unscannable_result(relative_path, STAGED_BLOB_REASON_GIT_SHOW_FAILED)
|
|
182
|
-
raw_bytes = completed_process.stdout
|
|
183
|
-
if len(raw_bytes) > MAXIMUM_STAGED_FILE_BYTES:
|
|
184
|
-
return _unscannable_result(relative_path, STAGED_BLOB_REASON_OVERSIZED)
|
|
185
|
-
if NULL_BYTE_MARKER in raw_bytes:
|
|
186
|
-
return _unscannable_result(relative_path, STAGED_BLOB_REASON_NULL_BYTES)
|
|
187
|
-
try:
|
|
188
|
-
return raw_bytes.decode(BODY_FILE_ENCODING), None
|
|
189
|
-
except UnicodeDecodeError:
|
|
190
|
-
return _unscannable_result(relative_path, STAGED_BLOB_REASON_DECODE_FAILED)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _scan_staged_path(
|
|
194
|
-
repository_root: Path, relative_path: str, all_allowlisted_values: frozenset[str]
|
|
195
|
-
) -> str | None:
|
|
196
|
-
"""Return a deny reason for one staged path, or None when it is clean."""
|
|
197
|
-
staged_text, unscannable_reason = read_staged_file_text(repository_root, relative_path)
|
|
198
|
-
if unscannable_reason is not None:
|
|
199
|
-
return unscannable_reason
|
|
200
|
-
if staged_text is None:
|
|
201
|
-
return _unscannable_result(relative_path, STAGED_BLOB_REASON_GIT_SHOW_FAILED)[1]
|
|
202
|
-
all_findings = [
|
|
203
|
-
each_finding
|
|
204
|
-
for each_finding in scan_text_for_pii(staged_text)
|
|
205
|
-
if each_finding.matched_text not in all_allowlisted_values
|
|
206
|
-
]
|
|
207
|
-
if not all_findings:
|
|
208
|
-
return None
|
|
209
|
-
return build_deny_reason(all_findings, f"staged commit ({relative_path})")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def evaluate_staged_commit(repository_root: Path) -> str | None:
|
|
213
|
-
"""Return a deny reason when staged content carries PII or is unscannable.
|
|
214
|
-
|
|
215
|
-
Git list or show failures and unscannable blobs deny the commit. A value
|
|
216
|
-
in the repository's PII allowlist is dropped from the findings, so its
|
|
217
|
-
commits may carry it.
|
|
218
|
-
|
|
219
|
-
Args:
|
|
220
|
-
repository_root: Repository whose index is about to be committed.
|
|
221
|
-
|
|
222
|
-
Returns:
|
|
223
|
-
Deny reason text, or None when every scannable staged path is clean.
|
|
224
|
-
"""
|
|
225
|
-
all_relative_paths, list_failure_reason = list_staged_file_paths(repository_root)
|
|
226
|
-
if list_failure_reason is not None or all_relative_paths is None:
|
|
227
|
-
return list_failure_reason or STAGED_LIST_FAILURE_REASON
|
|
228
|
-
all_allowlisted_values = repository_allowlisted_values(repository_root)
|
|
229
|
-
for each_relative_path in all_relative_paths:
|
|
230
|
-
if is_path_exempt_from_pii_scan(each_relative_path):
|
|
231
|
-
continue
|
|
232
|
-
deny_reason = _scan_staged_path(repository_root, each_relative_path, all_allowlisted_values)
|
|
233
|
-
if deny_reason is not None:
|
|
234
|
-
return deny_reason
|
|
235
|
-
return None
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _evaluate_commit_staged_content(bash_command: str, working_directory: str | None) -> str | None:
|
|
239
|
-
"""Scan the staged content of the repository the commit command targets.
|
|
240
|
-
|
|
241
|
-
Resolves the repository from the command's ``-C`` or ``cd`` path. Falls
|
|
242
|
-
back to the session working directory only when the command names none. A
|
|
243
|
-
removed cwd does not block a commit that targets a valid repo.
|
|
244
|
-
|
|
245
|
-
Args:
|
|
246
|
-
bash_command: The Bash or PowerShell tool command string.
|
|
247
|
-
working_directory: Session working directory fallback.
|
|
248
|
-
|
|
249
|
-
Returns:
|
|
250
|
-
Deny reason text, or None when the staged content is clean or exempt.
|
|
251
|
-
"""
|
|
252
|
-
command_directory = compose_command_working_directory(bash_command)
|
|
253
|
-
attempted_directory = command_directory if command_directory is not None else working_directory
|
|
254
|
-
expanded_directory = expand_user_directory(attempted_directory)
|
|
255
|
-
repository_root = resolve_repository_root(expanded_directory)
|
|
256
|
-
if repository_root is None:
|
|
257
|
-
return refusal_reason_for_unresolved_repository(attempted_directory)
|
|
258
|
-
if _is_repository_exempt_from_pii_scan(repository_root):
|
|
259
|
-
return None
|
|
260
|
-
return evaluate_staged_commit(repository_root)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def evaluate_bash_command(bash_command: str, working_directory: str | None) -> str | None:
|
|
264
|
-
"""Return a deny reason for a shell gh post or git commit with PII.
|
|
265
|
-
|
|
266
|
-
Args:
|
|
267
|
-
bash_command: The Bash or PowerShell tool command string.
|
|
268
|
-
working_directory: Session working directory, used only when the command
|
|
269
|
-
names no repository path.
|
|
270
|
-
|
|
271
|
-
Returns:
|
|
272
|
-
Deny reason text, or None when the command is clean or out of scope.
|
|
273
|
-
"""
|
|
274
|
-
all_post_bodies, body_file_failure_reason = extract_gh_post_body_texts_for_privacy_gate(
|
|
275
|
-
bash_command, working_directory=working_directory
|
|
276
|
-
)
|
|
277
|
-
if body_file_failure_reason is not None:
|
|
278
|
-
return body_file_failure_reason
|
|
279
|
-
post_deny_reason = evaluate_post_body_texts(all_post_bodies)
|
|
280
|
-
if post_deny_reason is not None:
|
|
281
|
-
return post_deny_reason
|
|
282
|
-
if not is_git_commit_shell_command(bash_command):
|
|
283
|
-
return None
|
|
284
|
-
return _evaluate_commit_staged_content(bash_command, working_directory)
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _evaluate_shell_tool(
|
|
288
|
-
all_tool_input: dict[str, object], payload_by_key: dict[str, object]
|
|
289
|
-
) -> str | None:
|
|
290
|
-
"""Resolve the command and working directory, then gate a shell tool call."""
|
|
291
|
-
command_value = all_tool_input.get("command", "")
|
|
292
|
-
if not isinstance(command_value, str) or not command_value:
|
|
293
|
-
return None
|
|
294
|
-
working_directory_value = all_tool_input.get("working_directory")
|
|
295
|
-
working_directory = (
|
|
296
|
-
working_directory_value if isinstance(working_directory_value, str) else None
|
|
297
|
-
)
|
|
298
|
-
if working_directory is None:
|
|
299
|
-
cwd_value = payload_by_key.get("cwd")
|
|
300
|
-
working_directory = cwd_value if isinstance(cwd_value, str) else None
|
|
301
|
-
return evaluate_bash_command(command_value, working_directory=working_directory)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
305
|
-
"""Decide whether a PreToolUse payload carries high-confidence PII.
|
|
306
|
-
|
|
307
|
-
Args:
|
|
308
|
-
payload_by_key: The PreToolUse payload with tool_name and tool_input.
|
|
309
|
-
|
|
310
|
-
Returns:
|
|
311
|
-
Deny-reason text when blocked, or None when allowed.
|
|
312
|
-
"""
|
|
313
|
-
raw_tool_name = payload_by_key.get("tool_name", "")
|
|
314
|
-
tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
|
|
315
|
-
raw_tool_input = payload_by_key.get("tool_input", {})
|
|
316
|
-
all_tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
|
|
317
|
-
if tool_name in ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES:
|
|
318
|
-
return evaluate_write_edit_payload(tool_name, all_tool_input, hook_payload=payload_by_key)
|
|
319
|
-
if tool_name == APPLY_PATCH_TOOL_NAME:
|
|
320
|
-
return evaluate_apply_patch_payload(all_tool_input, hook_payload=payload_by_key)
|
|
321
|
-
if tool_name in ALL_SHELL_TOOL_NAMES:
|
|
322
|
-
return _evaluate_shell_tool(all_tool_input, payload_by_key)
|
|
323
|
-
if tool_name.startswith(MCP_GITHUB_TOOL_PREFIX):
|
|
324
|
-
return evaluate_post_body_texts(extract_mcp_body_texts(all_tool_input))
|
|
325
|
-
return None
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def build_deny_payload(deny_reason: str) -> dict[str, object]:
|
|
329
|
-
"""Build the PreToolUse deny payload for *deny_reason*.
|
|
330
|
-
|
|
331
|
-
Args:
|
|
332
|
-
deny_reason: The permissionDecisionReason text.
|
|
333
|
-
|
|
334
|
-
Returns:
|
|
335
|
-
Deny payload dictionary serialized to stdout by the hook.
|
|
336
|
-
"""
|
|
337
|
-
log_hook_block(
|
|
338
|
-
calling_hook_name=HOOK_SCRIPT_BASENAME,
|
|
339
|
-
hook_event="PreToolUse",
|
|
340
|
-
block_reason=deny_reason,
|
|
341
|
-
)
|
|
342
|
-
return {
|
|
343
|
-
"hookSpecificOutput": {
|
|
344
|
-
"hookEventName": "PreToolUse",
|
|
345
|
-
"permissionDecision": "deny",
|
|
346
|
-
"permissionDecisionReason": deny_reason,
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def main() -> None:
|
|
352
|
-
"""Read PreToolUse stdin and deny when high-confidence PII is present."""
|
|
353
|
-
payload_dictionary = read_hook_input_dictionary_from_stdin()
|
|
354
|
-
if payload_dictionary is None:
|
|
355
|
-
sys.exit(0)
|
|
356
|
-
deny_reason = evaluate(payload_dictionary)
|
|
357
|
-
if deny_reason is None:
|
|
358
|
-
sys.exit(0)
|
|
359
|
-
sys.stdout.write(json.dumps(build_deny_payload(deny_reason)) + "\n")
|
|
360
|
-
sys.stdout.flush()
|
|
361
|
-
sys.exit(0)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if __name__ == "__main__":
|
|
365
|
-
main()
|