claude-dev-env 7.0.0 → 7.1.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.
Files changed (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +189 -0
  6. package/.agents/skills/session-continuity/SKILL.md +114 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +375 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +78 -0
  20. package/bin/install-session-continuity.test.mjs +42 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. 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()