claude-dev-env 1.92.1 → 1.93.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -2
- package/_shared/CLAUDE.md +2 -1
- package/_shared/advisor/CLAUDE.md +21 -0
- package/_shared/advisor/advisor-protocol.md +142 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
- package/_shared/advisor/scripts/pyproject.toml +3 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
- package/_shared/advisor/scripts/tier_model_ids.py +164 -0
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/precatch-rubric.md +65 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +260 -14
- package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +20 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
- package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
- package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
- package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +292 -2
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
- package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
- package/agents/CLAUDE.md +3 -2
- package/agents/clasp-deployment-orchestrator.md +2 -2
- package/agents/code-advisor.md +7 -5
- package/agents/code-verifier.md +6 -2
- package/agents/session-advisor.md +27 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
- package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
- package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
- package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
- package/bin/CLAUDE.md +1 -0
- package/bin/expand_home_directory_tokens.mjs +75 -0
- package/bin/install.mjs +21 -7
- package/bin/install.test.mjs +220 -10
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +4 -1
- package/hooks/blocking/_path_setup.py +13 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
- package/hooks/blocking/code_rules_enforcer.py +6 -0
- package/hooks/blocking/code_rules_imports_logging.py +34 -1
- package/hooks/blocking/code_rules_paths_syspath.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +272 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
- package/hooks/blocking/pii_prevention_blocker.py +781 -0
- package/hooks/blocking/pii_scanner.py +365 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
- package/hooks/blocking/stop_dispatcher.py +84 -0
- package/hooks/blocking/test__path_setup.py +39 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
- package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
- package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
- package/hooks/blocking/test_destructive_command_blocker.py +29 -9
- package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
- package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
- package/hooks/blocking/test_pii_scanner.py +190 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
- package/hooks/blocking/test_stop_dispatcher.py +187 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
- package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
- package/hooks/diagnostic/migrations/README.md +25 -26
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
- package/hooks/hooks.json +10 -120
- package/hooks/hooks_constants/CLAUDE.md +12 -3
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
- package/hooks/hooks_constants/local_identity.py +182 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
- package/hooks/hooks_constants/pii_prevention_constants.py +295 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
- package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
- package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
- package/hooks/hooks_constants/test_layout_constants.py +28 -0
- package/hooks/hooks_constants/test_local_identity.py +88 -0
- package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
- package/hooks/validators/README.md +5 -6
- package/hooks/validators/__init__.py +2 -2
- package/hooks/validators/python_style_checks.py +298 -243
- package/hooks/validators/run_all_validators.py +6 -0
- package/hooks/validators/test_python_style_checks.py +278 -163
- package/package.json +2 -2
- package/rules/CLAUDE.md +2 -1
- package/rules/bdd.md +1 -1
- package/rules/nas-ssh-invocation.md +6 -4
- package/rules/no-justification-noise.md +61 -0
- package/scripts/test_setup_project_paths.py +1 -1
- package/skills/CLAUDE.md +6 -3
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +113 -35
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +44 -22
- package/skills/autoconverge/reference/gotchas.md +11 -0
- package/skills/autoconverge/reference/stop-conditions.md +23 -13
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
- package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
- package/skills/autoconverge/workflow/converge.mjs +329 -68
- package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
- package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
- package/skills/bdd-protocol/SKILL.md +4 -5
- package/skills/bdd-protocol/references/anti-patterns.md +1 -1
- package/skills/bugteam/SKILL.md +8 -0
- package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +7 -5
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/copilot-finding-triage/SKILL.md +124 -0
- package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
- package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
- package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
- package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
- package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
- package/skills/copilot-finding-triage/templates/notification.md +34 -0
- package/skills/copilot-review/SKILL.md +8 -6
- package/skills/findbugs/SKILL.md +4 -0
- package/skills/fixbugs/SKILL.md +8 -7
- package/skills/gotcha/CLAUDE.md +2 -2
- package/skills/gotcha/SKILL.md +4 -4
- package/skills/log-audit/SKILL.md +4 -6
- package/skills/monitor-open-prs/CLAUDE.md +1 -1
- package/skills/monitor-open-prs/SKILL.md +6 -1
- package/skills/orchestrator/SKILL.md +74 -78
- package/skills/orchestrator-refresh/SKILL.md +24 -12
- package/skills/post-audit-findings/SKILL.md +5 -9
- package/skills/pr-consistency-audit/SKILL.md +5 -1
- package/skills/pr-converge/CLAUDE.md +1 -1
- package/skills/pr-converge/SKILL.md +86 -47
- package/skills/pr-converge/reference/CLAUDE.md +1 -1
- package/skills/pr-converge/reference/convergence-gates.md +20 -17
- package/skills/pr-converge/reference/examples.md +63 -47
- package/skills/pr-converge/reference/fix-protocol.md +15 -15
- package/skills/pr-converge/reference/ground-rules.md +11 -7
- package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
- package/skills/pr-converge/reference/per-tick.md +129 -107
- package/skills/pr-converge/reference/state-schema.md +15 -10
- package/skills/pr-converge/scripts/check_convergence.py +5 -4
- package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
- package/skills/pr-fix-protocol/SKILL.md +3 -8
- package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
- package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
- package/skills/pr-loop-lifecycle/SKILL.md +5 -9
- package/skills/pr-scope-resolve/SKILL.md +3 -6
- package/skills/privacy-hygiene/SKILL.md +114 -0
- package/skills/qbug/SKILL.md +8 -8
- package/skills/rebase/SKILL.md +5 -1
- package/skills/refine/SKILL.md +4 -5
- package/skills/reviewer-gates/SKILL.md +7 -11
- package/skills/session-log/SKILL.md +4 -1
- package/skills/skill-builder/SKILL.md +3 -6
- package/skills/structure-prompt/SKILL.md +4 -5
- package/skills/team-advisor/SKILL.md +56 -0
- package/skills/test_markdown_link_integrity.py +10 -6
- package/skills/update/SKILL.md +5 -1
- package/skills/usage-pause/SKILL.md +14 -5
- package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
- 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
|
|
5
|
-
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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 `
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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:**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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\\
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
46
|
-
"timeout":
|
|
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/
|
|
265
|
-
"timeout":
|
|
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,
|
|
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
|
-
-
|
|
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)
|