claude-dev-env 1.92.1 → 1.93.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 (214) hide show
  1. package/CLAUDE.md +6 -2
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +2 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/scripts/test_setup_project_paths.py +1 -1
  128. package/skills/CLAUDE.md +6 -3
  129. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  130. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  131. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  133. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  134. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  135. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  136. package/skills/autoconverge/CLAUDE.md +3 -3
  137. package/skills/autoconverge/SKILL.md +113 -35
  138. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  139. package/skills/autoconverge/reference/convergence.md +44 -22
  140. package/skills/autoconverge/reference/gotchas.md +11 -0
  141. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  142. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  143. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  144. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  145. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  146. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  147. package/skills/autoconverge/workflow/converge.mjs +329 -68
  148. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  149. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  150. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  151. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  152. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  153. package/skills/bdd-protocol/SKILL.md +4 -5
  154. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  155. package/skills/bugteam/SKILL.md +8 -0
  156. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  157. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  158. package/skills/bugteam/reference/team-setup.md +7 -5
  159. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  160. package/skills/copilot-finding-triage/SKILL.md +124 -0
  161. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  162. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  163. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  166. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  167. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  168. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  169. package/skills/copilot-review/SKILL.md +8 -6
  170. package/skills/findbugs/SKILL.md +4 -0
  171. package/skills/fixbugs/SKILL.md +8 -7
  172. package/skills/gotcha/CLAUDE.md +2 -2
  173. package/skills/gotcha/SKILL.md +4 -4
  174. package/skills/log-audit/SKILL.md +4 -6
  175. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  176. package/skills/monitor-open-prs/SKILL.md +6 -1
  177. package/skills/orchestrator/SKILL.md +74 -78
  178. package/skills/orchestrator-refresh/SKILL.md +24 -12
  179. package/skills/post-audit-findings/SKILL.md +5 -9
  180. package/skills/pr-consistency-audit/SKILL.md +5 -1
  181. package/skills/pr-converge/CLAUDE.md +1 -1
  182. package/skills/pr-converge/SKILL.md +86 -47
  183. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  184. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  185. package/skills/pr-converge/reference/examples.md +63 -47
  186. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  187. package/skills/pr-converge/reference/ground-rules.md +11 -7
  188. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  189. package/skills/pr-converge/reference/per-tick.md +129 -107
  190. package/skills/pr-converge/reference/state-schema.md +15 -10
  191. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  192. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  193. package/skills/pr-fix-protocol/SKILL.md +3 -8
  194. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  195. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  196. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  197. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  198. package/skills/pr-scope-resolve/SKILL.md +3 -6
  199. package/skills/privacy-hygiene/SKILL.md +114 -0
  200. package/skills/qbug/SKILL.md +8 -8
  201. package/skills/rebase/SKILL.md +5 -1
  202. package/skills/refine/SKILL.md +4 -5
  203. package/skills/reviewer-gates/SKILL.md +7 -11
  204. package/skills/session-log/SKILL.md +4 -1
  205. package/skills/skill-builder/SKILL.md +3 -6
  206. package/skills/structure-prompt/SKILL.md +4 -5
  207. package/skills/team-advisor/SKILL.md +56 -0
  208. package/skills/test_markdown_link_integrity.py +10 -6
  209. package/skills/update/SKILL.md +5 -1
  210. package/skills/usage-pause/SKILL.md +14 -5
  211. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  212. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  213. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  214. package/skills/verified-build/SKILL.md +4 -9
@@ -1,11 +1,11 @@
1
1
  # Hook-Log Diagnostic Migrations
2
2
 
3
3
  One-time DDL migrations for the hook-log diagnostic store. Each file is named
4
- `YYYY-MM-DD-<short-description>.sql` and contains idempotent statements that
5
- can be re-run without error.
4
+ `YYYY-MM-DD-<short-description>.sql` and has idempotent statements that can be
5
+ re-run without error.
6
6
 
7
- These files are records of operations already executed (or to be executed)
8
- against a specific Neon project. They are not run automatically by any hook.
7
+ These files are records of operations run (or to be run) against a specific
8
+ Neon project. No hook runs them automatically.
9
9
 
10
10
  ## How to apply
11
11
 
@@ -19,35 +19,34 @@ psql "$DATABASE_URL" -f packages/claude-dev-env/hooks/diagnostic/migrations/<fil
19
19
 
20
20
  When the project is hosted on Neon, the `mcp__neon__run_sql_transaction` tool
21
21
  accepts the same statements as a list of strings. That is the path used for
22
- the 2026-04-25 isolation migration described below, since the operator
23
- already has Neon MCP authenticated.
22
+ the 2026-04-25 isolation migration described below, since the operator already
23
+ has Neon MCP authenticated.
24
24
 
25
25
  ## 2026-04-25-drop-themes-hook-events.sql
26
26
 
27
- **Target:** Neon project `still-dust-13937951` ("Themes"). This is the
28
- production themes-asset-database project, which up to PR #257 was also
29
- holding the hook-log diagnostic table because the
30
- `NEON_HOOK_LOGS_DATABASE_URL` Bitwarden secret was pointing at it.
27
+ **Target:** Neon project `example-project-00000000`, a production application
28
+ database. This project also held the hook-log diagnostic table up to PR #257,
29
+ because the `NEON_HOOK_LOGS_DATABASE_URL` Bitwarden secret pointed at it.
31
30
 
32
31
  **Effect:** Drops the diagnostic view `blocked_commands` and the diagnostic
33
- table `hook_events` from the Themes project. The two objects were created
34
- by `schema.sql` during a misrouted live test in session 80; they share no
35
- foreign keys or other coupling with the themes-domain tables (`assets`,
36
- `themes`, `monthly_sales`, etc.) and removing them is a pure cleanup.
32
+ table `hook_events` from the source project. The two objects came from
33
+ `schema.sql` during a misrouted live test in session 80; they share no foreign
34
+ keys or other coupling with the project's own domain tables, and removing them
35
+ is a pure cleanup.
37
36
 
38
37
  **State before this migration:** `hook_events` holds 10,684 rows spanning
39
- 2026-04-12 through 2026-04-24. None of these rows are migrated; the
40
- "start fresh" decision (recorded in PR #261) means the extractor's
41
- `--full-rebuild` mode rebuilds the table contents from local JSONL on
42
- its next run against the new project.
38
+ 2026-04-12 through 2026-04-24. None of these rows move; the "start fresh"
39
+ decision (recorded in PR #261) means the extractor's `--full-rebuild` mode
40
+ rebuilds the table contents from local JSONL on its next run against the
41
+ hook-log project.
43
42
 
44
- **State after this migration:** Themes contains only its production
45
- domain tables. The hook-log diagnostic store lives entirely in the new
46
- Neon project `winter-haze-99075918` ("claude-hook-logs"), which the
47
- updated `NEON_HOOK_LOGS_DATABASE_URL` secret now points at.
43
+ **State after this migration:** the source project has only its own domain
44
+ tables. The hook-log diagnostic store lives entirely in a separate Neon project
45
+ `example-project-11111111`, which the `NEON_HOOK_LOGS_DATABASE_URL` secret
46
+ points at.
48
47
 
49
- **Verification:** Before applying, confirm there is no other consumer
50
- of `blocked_commands` or `hook_events` in the Themes project:
48
+ **Verification:** Before applying, confirm there is no other consumer of
49
+ `blocked_commands` or `hook_events` in the source project:
51
50
 
52
51
  ```sql
53
52
  SELECT table_name, view_definition
@@ -63,8 +62,8 @@ WHERE confrelid = 'hook_events'::regclass;
63
62
 
64
63
  The first query excludes `blocked_commands` itself, since that view (also dropped by this migration) references `hook_events` in its definition and would otherwise always match.
65
64
 
66
- Both queries should return zero rows; if either returns anything,
67
- investigate the dependency before running the drop.
65
+ Both queries should return zero rows; if either returns anything, investigate
66
+ the dependency before running the drop.
68
67
 
69
68
  After applying, confirm the objects are gone:
70
69
 
@@ -46,7 +46,7 @@ def _make_success_line(
46
46
  hook_name: str = "PreToolUse:Bash",
47
47
  hook_event: str = "PreToolUse",
48
48
  tool_use_id: str = "toolu_001",
49
- command: str = "python C:/Users/jon/.claude/hooks/blocking/destructive_command_blocker.py",
49
+ command: str = "python C:/Users/example/.claude/hooks/blocking/destructive_command_blocker.py",
50
50
  stdout: str = "ok\n",
51
51
  stderr: str = "",
52
52
  exit_code: int = 0,
@@ -82,7 +82,7 @@ def _make_blocking_line(
82
82
  hook_event: str = "PreToolUse",
83
83
  tool_use_id: str = "toolu_002",
84
84
  blocking_message: str = "blocked for reason",
85
- command: str = "python C:/Users/jon/.claude/hooks/blocking/block_main_commit.py",
85
+ command: str = "python C:/Users/example/.claude/hooks/blocking/block_main_commit.py",
86
86
  timestamp: str = "2026-04-24T13:32:54.293Z",
87
87
  cwd: str = "Y:\\Projects\\repo",
88
88
  git_branch: str = "main",
@@ -166,12 +166,12 @@ def _make_additional_context_line(
166
166
  sorted(KNOWN_HOOK_CATEGORIES),
167
167
  )
168
168
  def test_derive_category_accepts_each_known_category(expected_category: str) -> None:
169
- script_path = f"python C:/Users/jon/.claude/hooks/{expected_category}/some_hook.py"
169
+ script_path = f"python C:/Users/example/.claude/hooks/{expected_category}/some_hook.py"
170
170
  assert hook_log_extractor.derive_category(script_path) == expected_category
171
171
 
172
172
 
173
173
  def test_derive_category_returns_uncategorized_for_unknown_parent() -> None:
174
- script_path = "python C:/Users/jon/.claude/hooks/unheard_of_bucket/some_hook.py"
174
+ script_path = "python C:/Users/example/.claude/hooks/unheard_of_bucket/some_hook.py"
175
175
  assert (
176
176
  hook_log_extractor.derive_category(script_path) == HOOK_CATEGORY_UNCATEGORIZED
177
177
  )
@@ -183,12 +183,12 @@ def test_derive_category_returns_uncategorized_for_empty_path() -> None:
183
183
 
184
184
 
185
185
  def test_derive_category_handles_windows_backslash_paths() -> None:
186
- script_path = "python C:\\Users\\jon\\.claude\\hooks\\blocking\\destructive_command_blocker.py"
186
+ script_path = "python C:\\Users\\example\\.claude\\hooks\\blocking\\destructive_command_blocker.py"
187
187
  assert hook_log_extractor.derive_category(script_path) == "blocking"
188
188
 
189
189
 
190
190
  def test_derive_category_strips_python_launcher_prefix() -> None:
191
- script_path = "python3 /home/jon/.claude/hooks/session/code_rules_reminder.py"
191
+ script_path = "python3 /home/example/.claude/hooks/session/code_rules_reminder.py"
192
192
  assert hook_log_extractor.derive_category(script_path) == "session"
193
193
 
194
194
 
@@ -259,23 +259,23 @@ def test_derive_outcome_raises_on_unknown_type() -> None:
259
259
 
260
260
  def test_extract_script_path_from_success_record() -> None:
261
261
  record_json = _make_success_line(
262
- command="python C:/Users/jon/.claude/hooks/blocking/foo.py",
262
+ command="python C:/Users/example/.claude/hooks/blocking/foo.py",
263
263
  )
264
264
  parsed = json.loads(record_json)
265
265
  assert (
266
266
  hook_log_extractor.extract_script_path(parsed["attachment"])
267
- == "C:/Users/jon/.claude/hooks/blocking/foo.py"
267
+ == "C:/Users/example/.claude/hooks/blocking/foo.py"
268
268
  )
269
269
 
270
270
 
271
271
  def test_extract_script_path_from_blocking_record() -> None:
272
272
  record_json = _make_blocking_line(
273
- command="python3 /home/jon/.claude/hooks/blocking/bar.py",
273
+ command="python3 /home/example/.claude/hooks/blocking/bar.py",
274
274
  )
275
275
  parsed = json.loads(record_json)
276
276
  assert (
277
277
  hook_log_extractor.extract_script_path(parsed["attachment"])
278
- == "/home/jon/.claude/hooks/blocking/bar.py"
278
+ == "/home/example/.claude/hooks/blocking/bar.py"
279
279
  )
280
280
 
281
281
 
package/hooks/hooks.json CHANGED
@@ -38,107 +38,12 @@
38
38
  ]
39
39
  },
40
40
  {
41
- "matcher": "Bash",
41
+ "matcher": "Bash|PowerShell",
42
42
  "hooks": [
43
43
  {
44
44
  "type": "command",
45
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/es_exe_path_rewriter.py",
46
- "timeout": 10
47
- },
48
- {
49
- "type": "command",
50
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/destructive_command_blocker.py",
51
- "timeout": 10
52
- },
53
- {
54
- "type": "command",
55
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/gh_body_arg_blocker.py",
56
- "timeout": 10
57
- },
58
- {
59
- "type": "command",
60
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/nas_ssh_binary_enforcer.py",
61
- "timeout": 10
62
- },
63
- {
64
- "type": "command",
65
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/volatile_path_in_post_blocker.py",
66
- "timeout": 10
67
- },
68
- {
69
- "type": "command",
70
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/conventional_pr_title_gate.py",
71
- "timeout": 10
72
- },
73
- {
74
- "type": "command",
75
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/reviewer_spawn_gate.py",
76
- "timeout": 20
77
- },
78
- {
79
- "type": "command",
80
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/block_main_commit.py",
81
- "timeout": 15
82
- },
83
- {
84
- "type": "command",
85
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/precommit_code_rules_gate.py",
86
- "timeout": 150
87
- },
88
- {
89
- "type": "command",
90
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/session_edit_stage_gate.py",
91
- "timeout": 15
92
- },
93
- {
94
- "type": "command",
95
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_description_enforcer.py",
96
- "timeout": 30
97
- },
98
- {
99
- "type": "command",
100
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/test_preflight_check.py",
101
- "timeout": 10
102
- },
103
- {
104
- "type": "command",
105
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/convergence_gate_blocker.py",
106
- "timeout": 15
107
- },
108
- {
109
- "type": "command",
110
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/windows_rmtree_blocker.py",
111
- "timeout": 10
112
- },
113
- {
114
- "type": "command",
115
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/gh_pr_author_enforcer.py",
116
- "timeout": 30
117
- },
118
- {
119
- "type": "command",
120
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/verified_commit_gate.py",
121
- "timeout": 15
122
- },
123
- {
124
- "type": "command",
125
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/verdict_directory_write_blocker.py",
126
- "timeout": 10
127
- }
128
- ]
129
- },
130
- {
131
- "matcher": "PowerShell",
132
- "hooks": [
133
- {
134
- "type": "command",
135
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/verified_commit_gate.py",
136
- "timeout": 15
137
- },
138
- {
139
- "type": "command",
140
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/verdict_directory_write_blocker.py",
141
- "timeout": 10
45
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/bash_pre_tool_use_dispatcher.py",
46
+ "timeout": 400
142
47
  }
143
48
  ]
144
49
  },
@@ -159,6 +64,11 @@
159
64
  "type": "command",
160
65
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/volatile_path_in_post_blocker.py",
161
66
  "timeout": 10
67
+ },
68
+ {
69
+ "type": "command",
70
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pii_prevention_blocker.py",
71
+ "timeout": 20
162
72
  }
163
73
  ]
164
74
  },
@@ -261,28 +171,8 @@
261
171
  "hooks": [
262
172
  {
263
173
  "type": "command",
264
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/hedging_language_blocker.py",
265
- "timeout": 10
266
- },
267
- {
268
- "type": "command",
269
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/question_to_user_enforcer.py",
270
- "timeout": 10
271
- },
272
- {
273
- "type": "command",
274
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/intent_only_ending_blocker.py",
275
- "timeout": 10
276
- },
277
- {
278
- "type": "command",
279
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/session_handoff_blocker.py",
280
- "timeout": 10
281
- },
282
- {
283
- "type": "command",
284
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/diagnostic/hook_log_stop_wrapper.py",
285
- "timeout": 30
174
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/stop_dispatcher.py",
175
+ "timeout": 90
286
176
  }
287
177
  ]
288
178
  }
@@ -9,6 +9,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
9
9
  | `__init__.py` | Package marker (`# pragma: no-tdd-gate`) |
10
10
  | `any_type_config.py` | Config for the `Any`-type escape-hatch check |
11
11
  | `banned_identifiers_constants.py` | The set of banned short identifiers and banned function-name prefixes |
12
+ | `bash_pre_tool_use_dispatcher_constants.py` | Permission outcomes, tool-name sets, and the ordered hosted-hook roster for the Bash PreToolUse dispatcher (also covers PowerShell-shared gates) |
12
13
  | `blocking_check_limits.py` | Max issue counts and preview lengths for blocking hooks |
13
14
  | `bot_mention_comment_blocker_constants.py` | Patterns for detecting bot @-mentions in PR comments |
14
15
  | `claude_md_orphan_file_blocker_constants.py` | Table patterns, file extensions, scan budget, and block-message text for the CLAUDE.md orphan-file blocker |
@@ -34,16 +35,19 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
34
35
  | `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
35
36
  | `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
36
37
  | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
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 |
37
39
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
38
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 the NAS host, ssh user, and ssh port the ssh enforcer guards, resolved from the `CLAUDE_NAS_*` env vars or `~/.claude/local-identity.json` with placeholder defaults; also composes the enforcer's two deny messages that quote those values |
39
42
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
40
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 |
41
- | `nas_ssh_binary_enforcer_constants.py` | Bash tool name, the NAS address pattern, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, the batch-mode pattern, and the two deny messages for the NAS ssh binary enforcer |
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 |
42
45
  | `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
43
46
  | `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
44
47
  | `package_inventory_stale_blocker_constants.py` | Inventory document names, production code extensions, backtick token pattern, smallest inventory size, exempt names, scan budget, and block-message text for the package-inventory stale-entry blocker |
45
48
  | `paired_test_coverage_constants.py` | Test-directory name, stem-test filename affixes, test-file globs, exempt public-function names, scan budget, coverage threshold, and guidance text for the public-function paired-test coverage check |
46
49
  | `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
50
+ | `pii_prevention_constants.py` | Patterns, allowlists, path exemptions, and deny-message text for `pii_prevention_blocker` |
47
51
  | `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
48
52
  | `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
49
53
  | `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
@@ -53,6 +57,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
53
57
  | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
54
58
  | `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
55
59
  | `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
60
+ | `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
56
61
  | `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
57
62
  | `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
58
63
  | `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
@@ -61,11 +66,13 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
61
66
  | `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
62
67
  | `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
63
68
  | `state_description_blocker_constants.py` | The set of historical/comparative phrases the state-description blocker rejects, plus the docstring-extraction and mention-span patterns for its Python docstring scan |
69
+ | `stop_dispatcher_constants.py` | Ordered hosted-hook roster and Stop block payload field names for the Stop-chain dispatcher |
64
70
  | `stuttering_check_config.py` | Config for the stuttering (repeated-phrase) check |
65
71
  | `stuttering_import_binding_constants.py` | Import-binding patterns for the stuttering check |
66
72
  | `subprocess_budget_completeness_constants.py` | Required argument names for the subprocess-budget completeness check |
67
73
  | `sys_path_insert_constants.py` | Patterns for detecting unguarded `sys.path.insert` calls |
68
74
  | `task_list_loop_starter_constants.py` | The one-line task-list instruction and the full session-start directive text for the task-list loop starter hook |
75
+ | `test_layout_constants.py` | The named constants for the test-layout write-time checks |
69
76
  | `text_stripping.py` | `strip_code_and_quotes()` — shared helper that removes fenced code blocks, inline code, and blockquotes from prose, imported by the Stop-hook prose blockers |
70
77
  | `unused_module_import_constants.py` | Patterns for detecting unused module-level imports |
71
78
  | `volatile_path_in_post_blocker_constants.py` | Volatile path markers, affected `gh` post subcommands, MCP body param names, and the corrective message for the volatile-path post blocker |
@@ -74,7 +81,9 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
74
81
 
75
82
  ## Conventions
76
83
 
77
- - Every file in this package is a pure constants module — no side effects, no I/O.
84
+ - Most files in this package are pure constants modules — no side effects, no I/O.
78
85
  - Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
79
86
  - Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
80
- - `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they are shared across many hooks.
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 the private NAS values, keeping a committed placeholder default.
89
+
@@ -0,0 +1,78 @@
1
+ """Constants for the Bash and PowerShell PreToolUse dispatcher.
2
+
3
+ Holds the permission outcomes, the two tool-name sets, and the ordered hosted-hook
4
+ roster with each hook's applicable-tool set. The dispatcher imports these to
5
+ select and run the hooks that a Bash or PowerShell tool call fires.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ __all__ = [
13
+ "DENY_DECISION",
14
+ "ASK_DECISION",
15
+ "ALLOW_DECISION",
16
+ "HOOK_EVENT_NAME",
17
+ "REASON_JOIN_SEPARATOR",
18
+ "CONTEXT_JOIN_SEPARATOR",
19
+ "BASH_TOOL_NAME",
20
+ "POWERSHELL_TOOL_NAME",
21
+ "ALL_BASH_ONLY_TOOL_NAMES",
22
+ "ALL_BASH_AND_POWERSHELL_TOOL_NAMES",
23
+ "BashHostedHookEntry",
24
+ "ALL_BASH_HOSTED_HOOK_ENTRIES",
25
+ ]
26
+
27
+ DENY_DECISION = "deny"
28
+ ASK_DECISION = "ask"
29
+ ALLOW_DECISION = "allow"
30
+ HOOK_EVENT_NAME = "PreToolUse"
31
+ REASON_JOIN_SEPARATOR = " | "
32
+ CONTEXT_JOIN_SEPARATOR = "\n"
33
+
34
+ BASH_TOOL_NAME = "Bash"
35
+ POWERSHELL_TOOL_NAME = "PowerShell"
36
+
37
+ ALL_BASH_ONLY_TOOL_NAMES: frozenset[str] = frozenset({BASH_TOOL_NAME})
38
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES: frozenset[str] = frozenset({BASH_TOOL_NAME, POWERSHELL_TOOL_NAME})
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class BashHostedHookEntry:
43
+ """A single hosted hook with the tool names it applies to.
44
+
45
+ Attributes:
46
+ script_relative_path: Hook path relative to the hooks/ directory.
47
+ applicable_tool_names: Tool names this hook runs for. The dispatcher
48
+ skips the hook when the payload's tool is not in this set.
49
+ """
50
+
51
+ script_relative_path: str
52
+ applicable_tool_names: frozenset[str]
53
+
54
+
55
+ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
56
+ BashHostedHookEntry("blocking/es_exe_path_rewriter.py", ALL_BASH_ONLY_TOOL_NAMES),
57
+ BashHostedHookEntry("blocking/destructive_command_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
58
+ BashHostedHookEntry("blocking/gh_body_arg_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
59
+ BashHostedHookEntry("blocking/nas_ssh_binary_enforcer.py", ALL_BASH_ONLY_TOOL_NAMES),
60
+ BashHostedHookEntry("blocking/volatile_path_in_post_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
61
+ BashHostedHookEntry(
62
+ "blocking/pii_prevention_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
63
+ ),
64
+ BashHostedHookEntry("blocking/conventional_pr_title_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
65
+ BashHostedHookEntry("blocking/reviewer_spawn_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
66
+ BashHostedHookEntry("blocking/block_main_commit.py", ALL_BASH_ONLY_TOOL_NAMES),
67
+ BashHostedHookEntry("blocking/precommit_code_rules_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
68
+ BashHostedHookEntry("blocking/session_edit_stage_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
69
+ BashHostedHookEntry("blocking/pr_description_enforcer.py", ALL_BASH_ONLY_TOOL_NAMES),
70
+ BashHostedHookEntry("blocking/test_preflight_check.py", ALL_BASH_ONLY_TOOL_NAMES),
71
+ BashHostedHookEntry("blocking/convergence_gate_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
72
+ BashHostedHookEntry("blocking/windows_rmtree_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
73
+ BashHostedHookEntry("blocking/gh_pr_author_enforcer.py", ALL_BASH_ONLY_TOOL_NAMES),
74
+ BashHostedHookEntry("blocking/verified_commit_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
75
+ BashHostedHookEntry(
76
+ "blocking/verdict_directory_write_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
77
+ ),
78
+ )
@@ -14,6 +14,7 @@ GIT_DIRECTORY_NAME: str = ".git"
14
14
  MINIMUM_UPPER_SNAKE_LENGTH: int = 2
15
15
  MAX_DEAD_MODULE_CONSTANT_ISSUES: int = 25
16
16
  MAX_SCAN_ROOT_FILE_COUNT: int = 2000
17
+ MAX_SCAN_ROOT_READ_COUNT: int = 20000
17
18
  DEAD_MODULE_CONSTANT_GUIDANCE: str = (
18
19
  "module-level constant is defined here but never imported or read by any"
19
20
  " module in the enclosing package tree - remove the constant, or reference it"
@@ -0,0 +1,73 @@
1
+ """Shared in-process runner for dispatcher-hosted hooks.
2
+
3
+ Runs one hook script via runpy under __main__ with stdin, stdout, and argv
4
+ swapped to mimic a standalone invocation, captures what the hook writes to
5
+ stdout, and reports whether the hook raised. The runner carries no allow or deny
6
+ policy: the caller reads captured_stdout to learn the hook's decision and
7
+ applies its own aggregation, so a hook that raises contributes no decision,
8
+ matching a standalone hook whose uncaught exception exits nonzero without
9
+ blocking the tool call.
10
+
11
+ ::
12
+
13
+ run = run_hook_capturing_output("/hooks/blocking/gate.py", '{"tool_name": "Bash"}')
14
+ ok: run.captured_stdout carries the gate's deny JSON, run.did_crash is False
15
+ flag: run.did_crash is True when the gate raised, run.captured_stdout is ""
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import io
21
+ import runpy
22
+ import sys
23
+ import traceback
24
+ from dataclasses import dataclass
25
+
26
+
27
+ @dataclass
28
+ class HostedHookRun:
29
+ """Outcome of running one hosted hook in-process.
30
+
31
+ Attributes:
32
+ captured_stdout: The text the hook wrote to stdout during its run.
33
+ did_crash: True when the hook raised a non-SystemExit exception.
34
+ """
35
+
36
+ captured_stdout: str
37
+ did_crash: bool
38
+
39
+
40
+ def log_hosted_hook_crash(hook_script_path: str, error: Exception) -> None:
41
+ """Write a one-line crash summary for a hosted hook to stderr."""
42
+ formatted_traceback = traceback.format_exc().strip()
43
+ last_line = formatted_traceback.splitlines()[-1] if formatted_traceback else str(error)
44
+ error_type_name = type(error).__name__
45
+ sys.stderr.write(
46
+ f"[dispatcher] crash in {hook_script_path}: {error_type_name}: {error} | {last_line}\n"
47
+ )
48
+ sys.stderr.flush()
49
+
50
+
51
+ def run_hook_capturing_output(hook_script_path: str, payload_text: str) -> HostedHookRun:
52
+ """Run a hook in-process, returning its captured stdout and crash flag.
53
+
54
+ Swaps stdin/stdout/argv to mimic a standalone run, executes via runpy under
55
+ __main__, catches SystemExit and any other exception (logged as a crash), and
56
+ always restores the swapped streams and argv.
57
+ """
58
+ original_stdin, original_stdout, original_argv = sys.stdin, sys.stdout, sys.argv
59
+ captured_output = io.StringIO()
60
+ did_crash = False
61
+ try:
62
+ sys.stdin = io.StringIO(payload_text)
63
+ sys.stdout = captured_output
64
+ sys.argv = [hook_script_path]
65
+ runpy.run_path(hook_script_path, run_name="__main__")
66
+ except SystemExit:
67
+ pass
68
+ except Exception as error:
69
+ log_hosted_hook_crash(hook_script_path, error)
70
+ did_crash = True
71
+ finally:
72
+ sys.stdin, sys.stdout, sys.argv = original_stdin, original_stdout, original_argv
73
+ return HostedHookRun(captured_stdout=captured_output.getvalue(), did_crash=did_crash)