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.
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 +213 -0
  6. package/.agents/skills/session-continuity/SKILL.md +125 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +447 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +373 -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 +88 -0
  20. package/bin/install-session-continuity.test.mjs +55 -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,130 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Stop hook that blocks Claude responses asking the user questions in prose.
4
-
5
- User-directed questions must route through the AskUserQuestion tool so the user
6
- sees structured options with labels. When the final paragraph of the response
7
- ends with a question mark or contains a recognized preamble phrase
8
- ("would you like", "should I", "do you want", "want me to", etc.), Claude is
9
- forced to re-output the response with the ask moved into an AskUserQuestion
10
- tool call.
11
- """
12
-
13
- import json
14
- import re
15
- import sys
16
- from pathlib import Path
17
-
18
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
19
- if _hooks_dir not in sys.path:
20
- sys.path.insert(0, _hooks_dir)
21
-
22
- from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
23
- from hooks_constants.messages import USER_FACING_ASKUSERQUESTION_NOTICE # noqa: E402
24
- from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
25
-
26
-
27
- def extract_final_paragraph(text: str) -> str:
28
- """Return the last non-empty paragraph of the prose after stripping code and quotes."""
29
- paragraph_split_pattern = re.compile(r"\n\s*\n")
30
- prose_text = strip_code_and_quotes(text)
31
- candidate_paragraphs = [
32
- each_paragraph.strip()
33
- for each_paragraph in paragraph_split_pattern.split(prose_text)
34
- if each_paragraph.strip()
35
- ]
36
- if not candidate_paragraphs:
37
- return ""
38
- return candidate_paragraphs[-1]
39
-
40
-
41
- def find_user_directed_question_indicators(text: str) -> list[str]:
42
- """Return indicator names for every user-directed question signal in the final paragraph."""
43
- all_preamble_patterns = [
44
- (re.compile(regex_text, re.IGNORECASE), display_name)
45
- for regex_text, display_name in [
46
- (r"\bwould\s+you\s+like\b", "would you like"),
47
- (r"\bshould\s+i\b", "should i"),
48
- (r"\bdo\s+you\s+want\b", "do you want"),
49
- (r"\bwhich\s+would\s+you\s+prefer\b", "which would you prefer"),
50
- (r"\blet\s+me\s+know\s+if\b", "let me know if"),
51
- (r"\blet\s+me\s+know\s+which\b", "let me know which"),
52
- (r"\blet\s+me\s+know\s+whether\b", "let me know whether"),
53
- (r"\bplease\s+confirm\b", "please confirm"),
54
- (r"\bplease\s+let\s+me\s+know\b", "please let me know"),
55
- (r"\bwant\s+me\s+to\b", "want me to"),
56
- ]
57
- ]
58
- terminal_question_mark_pattern = re.compile(r"\?[\s\"'\)\]\}]*\Z")
59
- terminal_question_mark_indicator = "terminal question mark in final paragraph"
60
-
61
- final_paragraph = extract_final_paragraph(text)
62
- if not final_paragraph:
63
- return []
64
-
65
- matched_indicators: list[str] = []
66
-
67
- if terminal_question_mark_pattern.search(final_paragraph.rstrip()):
68
- matched_indicators.append(terminal_question_mark_indicator)
69
-
70
- for each_pattern, each_display_name in all_preamble_patterns:
71
- if (
72
- each_pattern.search(final_paragraph)
73
- and each_display_name not in matched_indicators
74
- ):
75
- matched_indicators.append(each_display_name)
76
-
77
- return matched_indicators
78
-
79
-
80
- def main() -> None:
81
- try:
82
- hook_input = json.load(sys.stdin)
83
- except json.JSONDecodeError:
84
- sys.exit(0)
85
-
86
- if hook_input.get("stop_hook_active", False):
87
- sys.exit(0)
88
-
89
- assistant_message = hook_input.get("last_assistant_message", "")
90
-
91
- if not assistant_message:
92
- sys.exit(0)
93
-
94
- matched_indicators = find_user_directed_question_indicators(assistant_message)
95
-
96
- if not matched_indicators:
97
- sys.exit(0)
98
-
99
- formatted_indicator_list = ", ".join(
100
- f'"{each_indicator}"' for each_indicator in matched_indicators
101
- )
102
-
103
- block_reason = (
104
- f"ASKUSERQUESTION GUARDRAIL: Your response asks the user a question in prose "
105
- f"(indicators: {formatted_indicator_list}). "
106
- f"User-directed questions must route through the AskUserQuestion tool so the user "
107
- f"sees structured options with labels.\n\n"
108
- f"Re-output your response with the trailing question removed from prose and moved "
109
- f"into an AskUserQuestion tool call. Rhetorical questions answered in the same "
110
- f"paragraph are allowed; questions inside code fences, inline code, and blockquotes "
111
- f"are ignored.\n\n"
112
- f"You MUST re-output the complete, revised response with the correction applied."
113
- )
114
- block_response = {
115
- "decision": "block",
116
- "reason": block_reason,
117
- "systemMessage": USER_FACING_ASKUSERQUESTION_NOTICE,
118
- "suppressOutput": True,
119
- }
120
- log_hook_block(
121
- calling_hook_name="question_to_user_enforcer.py",
122
- hook_event="Stop",
123
- block_reason=block_reason,
124
- )
125
- print(json.dumps(block_response))
126
- sys.exit(0)
127
-
128
-
129
- if __name__ == "__main__":
130
- main()
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook: block SendUserFile attaches that should open locally.
3
-
4
- SendUserFile attaches a file to the session. While the user is at the terminal
5
- (status "normal" or unset) an attach does not let them see the file — it must
6
- open on screen in its native Windows app. The corrective message names the
7
- ``Invoke-Item -LiteralPath`` form. The one attach allowed through is an
8
- away-from-desk phone push (status "proactive").
9
- """
10
-
11
- import json
12
- import sys
13
- from pathlib import Path
14
-
15
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
16
- if _hooks_dir not in sys.path:
17
- sys.path.insert(0, _hooks_dir)
18
-
19
- from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
20
- from hooks_constants.send_user_file_open_locally_blocker_constants import ( # noqa: E402
21
- CORRECTIVE_MESSAGE,
22
- PROACTIVE_STATUS,
23
- TOOL_NAME,
24
- )
25
-
26
-
27
- def _should_block(status: str) -> bool:
28
- """Return whether a SendUserFile call with this status should be denied.
29
-
30
- Args:
31
- status: The ``status`` field from the SendUserFile input. A proactive
32
- phone push is allowed; every other value, including an empty one,
33
- is a desk-side attach the user cannot see and is denied.
34
- """
35
- return status != PROACTIVE_STATUS
36
-
37
-
38
- def main() -> None:
39
- try:
40
- hook_input = json.load(sys.stdin)
41
- except json.JSONDecodeError:
42
- sys.exit(0)
43
-
44
- if hook_input.get("tool_name", "") != TOOL_NAME:
45
- sys.exit(0)
46
-
47
- tool_input = hook_input.get("tool_input") or {}
48
- status = tool_input.get("status", "")
49
- if not _should_block(status):
50
- sys.exit(0)
51
-
52
- deny_payload = {
53
- "hookSpecificOutput": {
54
- "hookEventName": "PreToolUse",
55
- "permissionDecision": "deny",
56
- "permissionDecisionReason": CORRECTIVE_MESSAGE,
57
- }
58
- }
59
- log_hook_block(
60
- calling_hook_name="send_user_file_open_locally_blocker.py",
61
- hook_event="PreToolUse",
62
- block_reason=CORRECTIVE_MESSAGE,
63
- tool_name=TOOL_NAME,
64
- )
65
- print(json.dumps(deny_payload))
66
- sys.stdout.flush()
67
- sys.exit(0)
68
-
69
-
70
- if __name__ == "__main__":
71
- main()
@@ -1,167 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Deny a Write, Edit, or MultiEdit whose target filename names a secret or a lock file.
3
-
4
- ::
5
-
6
- .env flag: a live secrets file
7
- .env.local flag: a live per-machine secrets file
8
- credentials.json flag: a live credential file
9
- id_ed25519 flag: a live private key
10
- package-lock.json flag: regenerated by its package manager, never hand-edited
11
-
12
- .env.example ok: a placeholders-only template, committed on purpose
13
- .env.sample ok: the same shape, .sample spelling
14
- .env.template ok: the same shape, .template spelling
15
- main.py ok: ordinary source
16
-
17
- A basename whose FINAL suffix is ``.example``, ``.sample``, or ``.template``
18
- names a committed template, so the filename guard steps aside for it. The
19
- suffix has to come last: ``.env.example.bak`` is a copy of a live secrets
20
- file, not a template, and stays denied.
21
-
22
- The guard over a write's contents stays in force: the ``pii_prevention_blocker``
23
- hook scans what a write carries, so a template holding a real secret or a real
24
- email address is denied on its contents. The two hooks are layered, and this
25
- one rules on the filename alone.
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- import fnmatch
31
- import json
32
- import os
33
- import sys
34
-
35
- import _path_setup # noqa: F401
36
-
37
- from codex_apply_patch import payload_patch_target_paths
38
- from hooks_constants.hook_block_logger import log_hook_block
39
- from hooks_constants.pre_tool_use_dispatcher_constants import (
40
- ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES,
41
- APPLY_PATCH_TOOL_NAME,
42
- )
43
- from hooks_constants.sensitive_file_protector_constants import (
44
- ALL_SENSITIVE_PATTERNS,
45
- ALL_TEMPLATE_SUFFIXES,
46
- DENY_DECISION,
47
- DENY_REASON_TEMPLATE,
48
- HOOK_EVENT_NAME,
49
- HOOK_SCRIPT_NAME,
50
- )
51
-
52
-
53
- def is_template_filename(filename: str) -> bool:
54
- """Report whether a basename names a placeholders-only committed template.
55
-
56
- ::
57
-
58
- .env.example -> True
59
- .ENV.Template -> True
60
- .env.example.bak -> False
61
- .env.local -> False
62
-
63
- Args:
64
- filename: The basename of the file a Write, Edit, or MultiEdit targets.
65
-
66
- Returns:
67
- True when the basename's final suffix is a template suffix.
68
- """
69
- return filename.lower().endswith(ALL_TEMPLATE_SUFFIXES)
70
-
71
-
72
- def is_sensitive_file(file_path: str) -> str | None:
73
- """Return the sensitive pattern a path's basename matches, or None.
74
-
75
- Args:
76
- file_path: The path the Write, Edit, or MultiEdit targets.
77
-
78
- Returns:
79
- The matched pattern, or None when the basename names a template or
80
- matches no pattern.
81
- """
82
- filename = os.path.basename(file_path)
83
- if is_template_filename(filename):
84
- return None
85
- for each_pattern in ALL_SENSITIVE_PATTERNS:
86
- if fnmatch.fnmatch(filename.lower(), each_pattern.lower()):
87
- return each_pattern
88
- return None
89
-
90
-
91
- def build_deny_response(deny_reason: str) -> dict[str, dict[str, str]]:
92
- """Wrap a deny reason in the PreToolUse decision payload Claude Code reads.
93
-
94
- Args:
95
- deny_reason: The operator-facing sentence explaining the denial.
96
-
97
- Returns:
98
- The hook decision dictionary carrying the deny permission decision.
99
- """
100
- return {
101
- "hookSpecificOutput": {
102
- "hookEventName": HOOK_EVENT_NAME,
103
- "permissionDecision": DENY_DECISION,
104
- "permissionDecisionReason": deny_reason,
105
- }
106
- }
107
-
108
-
109
- def deny_write(file_path: str, matched_pattern: str) -> None:
110
- """Record the block in the hook-blocks log and emit the deny decision.
111
-
112
- Args:
113
- file_path: The path the Write, Edit, MultiEdit, or apply_patch targets.
114
- matched_pattern: The sensitive pattern the basename matched.
115
- """
116
- deny_reason = DENY_REASON_TEMPLATE.format(
117
- filename=os.path.basename(file_path),
118
- matched_pattern=matched_pattern,
119
- )
120
- log_hook_block(
121
- calling_hook_name=HOOK_SCRIPT_NAME,
122
- hook_event=HOOK_EVENT_NAME,
123
- block_reason=deny_reason,
124
- offending_input_preview=file_path,
125
- )
126
- sys.stdout.write(json.dumps(build_deny_response(deny_reason)))
127
-
128
-
129
- def _deny_apply_patch_sensitive_target(hook_input: dict, tool_input: dict) -> None:
130
- """Deny the first apply_patch target path whose basename names a sensitive file."""
131
- for each_target_path in payload_patch_target_paths(hook_input, tool_input):
132
- matched_pattern = is_sensitive_file(each_target_path)
133
- if matched_pattern is not None:
134
- deny_write(each_target_path, matched_pattern)
135
- return
136
-
137
-
138
- def main() -> None:
139
- """Deny the write when the stdin payload targets a sensitive filename."""
140
- try:
141
- hook_input = json.load(sys.stdin)
142
- except json.JSONDecodeError:
143
- sys.exit(0)
144
-
145
- tool_name = hook_input.get("tool_name", "")
146
- tool_input = hook_input.get("tool_input", {})
147
-
148
- if tool_name not in ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES:
149
- sys.exit(0)
150
-
151
- if tool_name == APPLY_PATCH_TOOL_NAME:
152
- _deny_apply_patch_sensitive_target(hook_input, tool_input)
153
- sys.exit(0)
154
-
155
- file_path = tool_input.get("file_path", "")
156
- if not file_path:
157
- sys.exit(0)
158
-
159
- matched_pattern = is_sensitive_file(file_path)
160
- if matched_pattern is not None:
161
- deny_write(file_path, matched_pattern)
162
-
163
- sys.exit(0)
164
-
165
-
166
- if __name__ == "__main__":
167
- main()