claude-dev-env 1.74.0 → 1.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +3 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +36 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +2 -1
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
- package/hooks/blocking/hedging_language_blocker.py +1 -13
- package/hooks/blocking/intent_only_ending_blocker.py +1 -15
- package/hooks/blocking/pre_tool_use_dispatcher.py +4 -5
- package/hooks/blocking/question_to_user_enforcer.py +1 -11
- package/hooks/blocking/session_handoff_blocker.py +1 -15
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +16 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
- package/hooks/blocking/test_hedging_language_blocker.py +6 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +52 -5
- package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
- package/hooks/blocking/test_session_handoff_blocker.py +6 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/CLAUDE.md +4 -1
- package/hooks/hooks_constants/blocking_check_limits.py +30 -1
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +8 -2
- package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
- package/hooks/hooks_constants/test_text_stripping.py +39 -0
- package/hooks/hooks_constants/text_stripping.py +36 -0
- package/hooks/validation/CLAUDE.md +1 -0
- package/hooks/validation/post_tool_use_dispatcher.py +2 -2
- package/hooks/validation/test_mypy_validator.py +1 -1
- package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
- package/hooks/workflow/auto_formatter.py +8 -5
- package/hooks/workflow/test_auto_formatter.py +33 -0
- package/package.json +1 -1
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +1 -0
- package/rules/package-inventory-stale-entry.md +8 -0
- package/rules/windows-filesystem-safe.md +2 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/SKILL.md +6 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +306 -137
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -44
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
- package/skills/autoconverge/workflow/converge.mjs +581 -613
- package/skills/autoconverge/workflow/convergence_summary.py +1 -1
- package/skills/autoconverge/workflow/render_report.py +2 -6
- package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
- package/skills/autoconverge/workflow/test_render_report.py +1 -0
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
|
@@ -418,7 +418,7 @@ def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
|
|
|
418
418
|
)
|
|
419
419
|
|
|
420
420
|
|
|
421
|
-
def
|
|
421
|
+
def test_workflow_verdict_covers_surface_false_for_missing_sidecar_with_verdict(
|
|
422
422
|
tmp_path: pathlib.Path,
|
|
423
423
|
) -> None:
|
|
424
424
|
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
@@ -438,6 +438,71 @@ def test_workflow_verdict_covers_surface_false_for_missing_sidecar(
|
|
|
438
438
|
)
|
|
439
439
|
|
|
440
440
|
|
|
441
|
+
def test_workflow_verdict_covers_surface_false_for_corrupt_sidecar_with_verdict(
|
|
442
|
+
tmp_path: pathlib.Path,
|
|
443
|
+
) -> None:
|
|
444
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
445
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
446
|
+
_write_agent_transcript(
|
|
447
|
+
subagents_dir,
|
|
448
|
+
"01",
|
|
449
|
+
VERIFIER_AGENT_TYPE,
|
|
450
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
451
|
+
should_write_sidecar=False,
|
|
452
|
+
)
|
|
453
|
+
corrupt_sidecar = (
|
|
454
|
+
subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
|
|
455
|
+
)
|
|
456
|
+
corrupt_sidecar.write_text("{not valid json", encoding="utf-8")
|
|
457
|
+
assert (
|
|
458
|
+
workflow_verdict_covers_surface(
|
|
459
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
460
|
+
)
|
|
461
|
+
is False
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_workflow_verdict_covers_surface_false_for_invalid_utf8_sidecar(
|
|
466
|
+
tmp_path: pathlib.Path,
|
|
467
|
+
) -> None:
|
|
468
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
469
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
470
|
+
_write_agent_transcript(
|
|
471
|
+
subagents_dir,
|
|
472
|
+
"01",
|
|
473
|
+
VERIFIER_AGENT_TYPE,
|
|
474
|
+
_verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
|
|
475
|
+
should_write_sidecar=False,
|
|
476
|
+
)
|
|
477
|
+
invalid_utf8_sidecar = subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
|
|
478
|
+
invalid_utf8_sidecar.write_bytes(b'{"agentType": "\xff\xfe bad"}')
|
|
479
|
+
assert (
|
|
480
|
+
workflow_verdict_covers_surface(
|
|
481
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
482
|
+
)
|
|
483
|
+
is False
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_workflow_verdict_covers_surface_false_for_invalid_utf8_transcript(
|
|
488
|
+
tmp_path: pathlib.Path,
|
|
489
|
+
) -> None:
|
|
490
|
+
transcript_path = _session_transcript_path(tmp_path, "sess1")
|
|
491
|
+
subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
|
|
492
|
+
workflow_dir = subagents_dir / "workflows" / "wf_x"
|
|
493
|
+
workflow_dir.mkdir(parents=True, exist_ok=True)
|
|
494
|
+
(workflow_dir / "agent-01.jsonl").write_bytes(b'{"type": "assistant"}\xff\xfe\n')
|
|
495
|
+
(workflow_dir / "agent-01.meta.json").write_text(
|
|
496
|
+
json.dumps({"agentType": VERIFIER_AGENT_TYPE}), encoding="utf-8"
|
|
497
|
+
)
|
|
498
|
+
assert (
|
|
499
|
+
workflow_verdict_covers_surface(
|
|
500
|
+
str(transcript_path), MATCHING_MANIFEST_SHA256
|
|
501
|
+
)
|
|
502
|
+
is False
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
441
506
|
def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
|
|
442
507
|
tmp_path: pathlib.Path,
|
|
443
508
|
) -> None:
|
|
@@ -7,11 +7,13 @@ SubagentStop payload names the stopping subagent's own transcript
|
|
|
7
7
|
tests build that sidecar and assert the minter gates on the resolved type and
|
|
8
8
|
on the shared MINTING_AGENT_TYPE constant, so a rename in config propagates to
|
|
9
9
|
the minter without a second edit. A malformed or non-string sidecar resolves
|
|
10
|
-
nothing, and an absent sidecar mints nothing
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
nothing, and an absent sidecar mints nothing even when the transcript carries a
|
|
11
|
+
verdict fence — the main session controls the prompt of any Agent-tool subagent
|
|
12
|
+
it spawns, so a verdict fence in the transcript proves nothing about the agent
|
|
13
|
+
type, and only the harness-written sidecar attests a code-verifier. A further
|
|
14
|
+
test holds the shipped settings.json to the minter docstring's anti-forgery
|
|
15
|
+
claim: the main session is denied writes to the verdict directory, so only this
|
|
16
|
+
hook can mint a passing verdict.
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
19
|
import importlib.util
|
|
@@ -290,6 +292,63 @@ def test_minter_refuses_when_recomputed_surface_is_empty(
|
|
|
290
292
|
assert mint_for_payload(payload) is None
|
|
291
293
|
|
|
292
294
|
|
|
295
|
+
def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
|
|
296
|
+
tmp_path: pathlib.Path,
|
|
297
|
+
) -> None:
|
|
298
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
299
|
+
agent_transcript.write_text(
|
|
300
|
+
json.dumps(
|
|
301
|
+
{
|
|
302
|
+
"type": "assistant",
|
|
303
|
+
"message": {
|
|
304
|
+
"content": [
|
|
305
|
+
{
|
|
306
|
+
"type": "text",
|
|
307
|
+
"text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
)
|
|
313
|
+
+ "\n",
|
|
314
|
+
encoding="utf-8",
|
|
315
|
+
)
|
|
316
|
+
payload = {"agent_transcript_path": str(agent_transcript)}
|
|
317
|
+
assert resolved_subagent_type(payload) is None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def test_does_not_mint_when_sidecar_absent_but_transcript_has_verdict(
|
|
321
|
+
tmp_path: pathlib.Path,
|
|
322
|
+
) -> None:
|
|
323
|
+
repo_root = tmp_path / "repo"
|
|
324
|
+
repo_root.mkdir()
|
|
325
|
+
_init_repo_with_upstream_and_edit(repo_root)
|
|
326
|
+
agent_transcript = tmp_path / "agent-7.jsonl"
|
|
327
|
+
agent_transcript.write_text(
|
|
328
|
+
json.dumps(
|
|
329
|
+
{
|
|
330
|
+
"type": "assistant",
|
|
331
|
+
"message": {
|
|
332
|
+
"content": [
|
|
333
|
+
{
|
|
334
|
+
"type": "text",
|
|
335
|
+
"text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
+ "\n",
|
|
342
|
+
encoding="utf-8",
|
|
343
|
+
)
|
|
344
|
+
payload = {
|
|
345
|
+
"agent_transcript_path": str(agent_transcript),
|
|
346
|
+
"cwd": str(repo_root),
|
|
347
|
+
"agent_id": "no-sidecar-1",
|
|
348
|
+
}
|
|
349
|
+
assert mint_for_payload(payload) is None
|
|
350
|
+
|
|
351
|
+
|
|
293
352
|
def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -> None:
|
|
294
353
|
repo_root = tmp_path / "repo"
|
|
295
354
|
repo_root.mkdir()
|
|
@@ -423,17 +423,29 @@ def _subagents_directory_for_transcript(transcript_path: str) -> Path | None:
|
|
|
423
423
|
def _agent_type_for_transcript(transcript_file: Path) -> str | None:
|
|
424
424
|
"""Read an agent transcript's sidecar to learn the agent type it ran as.
|
|
425
425
|
|
|
426
|
+
The agent type comes solely from the harness-written
|
|
427
|
+
``agent-<id>.meta.json`` sidecar, which only the harness writes — the main
|
|
428
|
+
session can neither write the sidecar nor forge the agentType it records. A
|
|
429
|
+
verdict fence in the transcript is never used to infer the type, because the
|
|
430
|
+
main session controls the prompt of any Agent-tool subagent it spawns and
|
|
431
|
+
could instruct one to print such a fence.
|
|
432
|
+
|
|
426
433
|
Args:
|
|
427
434
|
transcript_file: An ``agent-*.jsonl`` transcript path.
|
|
428
435
|
|
|
429
436
|
Returns:
|
|
430
|
-
The ``agentType`` recorded in the ``<stem>.meta.json`` sidecar, or
|
|
431
|
-
|
|
437
|
+
The ``agentType`` recorded in the ``<stem>.meta.json`` sidecar, or None
|
|
438
|
+
when the sidecar is absent, unreadable, or unparseable, it does not hold
|
|
439
|
+
a JSON object, or it names no string ``agentType``.
|
|
432
440
|
"""
|
|
433
441
|
sidecar_file = transcript_file.with_suffix(AGENT_META_SIDECAR_SUFFIX)
|
|
434
442
|
try:
|
|
435
|
-
|
|
436
|
-
except (OSError,
|
|
443
|
+
sidecar_text = sidecar_file.read_text(encoding="utf-8")
|
|
444
|
+
except (OSError, UnicodeDecodeError):
|
|
445
|
+
return None
|
|
446
|
+
try:
|
|
447
|
+
sidecar_record = json.loads(sidecar_text)
|
|
448
|
+
except json.JSONDecodeError:
|
|
437
449
|
return None
|
|
438
450
|
if not isinstance(sidecar_record, dict):
|
|
439
451
|
return None
|
|
@@ -452,7 +464,9 @@ def _assistant_text_blocks(transcript_file: Path) -> list[str]:
|
|
|
452
464
|
when the file is missing, unreadable, or holds no assistant text.
|
|
453
465
|
"""
|
|
454
466
|
try:
|
|
455
|
-
transcript_lines =
|
|
467
|
+
transcript_lines = (
|
|
468
|
+
transcript_file.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
469
|
+
)
|
|
456
470
|
except OSError:
|
|
457
471
|
return []
|
|
458
472
|
all_text_blocks: list[str] = []
|
|
@@ -6,14 +6,14 @@ SubagentStop payload names the stopping subagent's own transcript
|
|
|
6
6
|
(``agent_transcript_path``), which sits beside a harness-written
|
|
7
7
|
``agent-<id>.meta.json`` sidecar naming the spawning ``agentType``. The hook
|
|
8
8
|
reads that type from the sidecar, so it resolves identically in interactive,
|
|
9
|
-
background, and worktree-switched sessions
|
|
9
|
+
background, and worktree-switched sessions; only the harness writes the
|
|
10
|
+
sidecar, so the type it records cannot be forged. When that type is
|
|
10
11
|
``code-verifier``, the hook pulls the verdict block out of the agent's own
|
|
11
|
-
transcript (``agent_transcript_path``)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
matches the verified state.
|
|
12
|
+
transcript (``agent_transcript_path``), recomputes the live change-surface
|
|
13
|
+
hash for the session repository, and writes the verdict bound to that hash.
|
|
14
|
+
The companion ``verified_commit_gate.py`` (PreToolUse) then allows ``git
|
|
15
|
+
commit`` / ``git push`` only while the work tree still matches the verified
|
|
16
|
+
state.
|
|
17
17
|
|
|
18
18
|
The verifier's final message must end with a fenced block::
|
|
19
19
|
|
|
@@ -125,7 +125,8 @@ def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
|
|
|
125
125
|
``agent-<id>.meta.json`` naming the spawning ``agentType``. Reading the type
|
|
126
126
|
from this sidecar binds it to the stopping subagent itself, so it resolves
|
|
127
127
|
identically in interactive, background, and worktree-switched sessions and
|
|
128
|
-
needs no parent-transcript scan or flush retry.
|
|
128
|
+
needs no parent-transcript scan or flush retry. Only the harness writes this
|
|
129
|
+
sidecar, so a session cannot forge the type it reports.
|
|
129
130
|
|
|
130
131
|
Args:
|
|
131
132
|
agent_transcript_path: The stopping subagent's own transcript path from
|
|
@@ -133,7 +134,7 @@ def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
|
|
|
133
134
|
|
|
134
135
|
Returns:
|
|
135
136
|
The recorded ``agentType``, or None when the path is empty, the sidecar
|
|
136
|
-
is absent
|
|
137
|
+
is absent, unreadable, or unparseable, it does not hold a JSON object,
|
|
137
138
|
or it names no string ``agentType``.
|
|
138
139
|
"""
|
|
139
140
|
if not agent_transcript_path:
|
|
@@ -155,17 +156,19 @@ def resolved_subagent_type(subagent_stop_payload: dict) -> str | None:
|
|
|
155
156
|
|
|
156
157
|
The stopping subagent's own transcript (``agent_transcript_path``) sits
|
|
157
158
|
beside a harness-written ``agent-<id>.meta.json`` sidecar naming its
|
|
158
|
-
``agentType``.
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
``agentType``. The type comes solely from that sidecar, which only the
|
|
160
|
+
harness writes — the main session can neither write the sidecar nor forge
|
|
161
|
+
the agentType it records. A verdict fence in the transcript is never used to
|
|
162
|
+
infer the type, because the main session controls the prompt of any
|
|
163
|
+
Agent-tool subagent it spawns and could instruct one to print such a fence.
|
|
161
164
|
|
|
162
165
|
Args:
|
|
163
166
|
subagent_stop_payload: The SubagentStop hook payload.
|
|
164
167
|
|
|
165
168
|
Returns:
|
|
166
|
-
The agent type
|
|
167
|
-
``agent_transcript_path`` is empty, the sidecar is absent
|
|
168
|
-
|
|
169
|
+
The agent type the sidecar records, or None when the
|
|
170
|
+
``agent_transcript_path`` is empty, the sidecar is absent, unreadable,
|
|
171
|
+
or unparseable, it does not hold a JSON object, or it names no string
|
|
169
172
|
``agentType``.
|
|
170
173
|
"""
|
|
171
174
|
return _agent_type_from_meta_sidecar(
|
|
@@ -24,9 +24,11 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
24
24
|
| `doc_gist_auto_publish_constants.py` | Sentinel marker and URL patterns for the doc-gist auto-publish hook |
|
|
25
25
|
| `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
|
|
26
26
|
| `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
|
|
27
|
+
| `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
|
|
27
28
|
| `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
|
|
28
29
|
| `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
|
|
29
30
|
| `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
|
|
31
|
+
| `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` |
|
|
30
32
|
| `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
|
|
31
33
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
32
34
|
| `html_companion_constants.py` | Blocked URL schemes and other config for the `.md`-to-`.html` companion hook |
|
|
@@ -55,6 +57,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
55
57
|
| `stuttering_import_binding_constants.py` | Import-binding patterns for the stuttering check |
|
|
56
58
|
| `subprocess_budget_completeness_constants.py` | Required argument names for the subprocess-budget completeness check |
|
|
57
59
|
| `sys_path_insert_constants.py` | Patterns for detecting unguarded `sys.path.insert` calls |
|
|
60
|
+
| `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 |
|
|
58
61
|
| `unused_module_import_constants.py` | Patterns for detecting unused module-level imports |
|
|
59
62
|
| `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
|
|
60
63
|
| `workflow_substitution_slot_blocker_constants.py` | Per-iteration token patterns for the workflow-slot blocker |
|
|
@@ -64,4 +67,4 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
64
67
|
- Every file in this package is a pure constants module — no side effects, no I/O.
|
|
65
68
|
- Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
|
|
66
69
|
- Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
|
|
67
|
-
- `dynamic_stderr_handler.py` and `
|
|
70
|
+
- `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.
|
|
@@ -7,7 +7,6 @@ under the file-global-constants use-count rule (CODE_RULES §file-global-constan
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
MAX_BANNED_PREFIX_ISSUES: int = 3
|
|
12
11
|
MAX_STUB_IMPLEMENTATION_ISSUES: int = 3
|
|
13
12
|
MAX_TYPED_DICT_PAIR_ISSUES: int = 3
|
|
@@ -24,8 +23,27 @@ MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
|
|
|
24
23
|
MAX_THIN_WRAPPER_ISSUES: int = 1
|
|
25
24
|
MAX_ZERO_PAYLOAD_ALIAS_ISSUES: int = 3
|
|
26
25
|
MAX_LOGGING_FSTRING_ISSUES: int = 3
|
|
26
|
+
MAX_LOGGING_PRINTF_TOKEN_ISSUES: int = 3
|
|
27
27
|
MAX_WINDOWS_API_NONE_ISSUES: int = 3
|
|
28
28
|
MAX_E2E_TEST_NAMING_ISSUES: int = 3
|
|
29
|
+
MAX_IMPORT_BLOCK_SORT_ISSUES: int = 1
|
|
30
|
+
IMPORT_BLOCK_SORT_RUFF_TIMEOUT_SECONDS: int = 15
|
|
31
|
+
IMPORT_BLOCK_SORT_RULE_CODE: str = "I001"
|
|
32
|
+
RUFF_STDIN_ENCODING: str = "utf-8"
|
|
33
|
+
RUFF_PYPROJECT_CONFIG_FILENAME: str = "pyproject.toml"
|
|
34
|
+
RUFF_PYPROJECT_TOOL_TABLE_MARKER: str = "[tool.ruff"
|
|
35
|
+
ALL_RUFF_STANDALONE_CONFIG_FILENAMES: tuple[str, ...] = ("ruff.toml", ".ruff.toml")
|
|
36
|
+
ALL_IMPORT_BLOCK_SORT_RUFF_COMMAND_PREFIX: tuple[str, ...] = (
|
|
37
|
+
"ruff",
|
|
38
|
+
"check",
|
|
39
|
+
"--select",
|
|
40
|
+
"I001",
|
|
41
|
+
"--no-cache",
|
|
42
|
+
"--output-format",
|
|
43
|
+
"json",
|
|
44
|
+
)
|
|
45
|
+
MAX_JS_RESUME_TASK_ENUMERATION_ISSUES: int = 5
|
|
46
|
+
MINIMUM_RESUME_TASK_ENUMERATION_ITEMS: int = 2
|
|
29
47
|
DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
|
|
30
48
|
MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
|
|
31
49
|
DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT: int = 2
|
|
@@ -85,6 +103,17 @@ DOCSTRING_REFERENCE_MARKER_WINDOW: int = 2
|
|
|
85
103
|
ALL_GENERIC_CHECK_NAME_TOKENS: frozenset[str] = frozenset(
|
|
86
104
|
{"check", "checks", "test", "tests", "in", "for", "and", "the"}
|
|
87
105
|
)
|
|
106
|
+
ALL_FORMAT_LOGGER_FUNCTION_NAMES: frozenset[str] = frozenset(
|
|
107
|
+
{
|
|
108
|
+
"log_debug",
|
|
109
|
+
"log_info",
|
|
110
|
+
"log_ok",
|
|
111
|
+
"log_error",
|
|
112
|
+
"log_warning",
|
|
113
|
+
"log_batch",
|
|
114
|
+
"log_background",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
88
117
|
|
|
89
118
|
ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
|
|
90
119
|
"no consumer reads",
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"""Constants for the per-directory CLAUDE.md orphan-file-reference blocker.
|
|
2
2
|
|
|
3
3
|
A per-directory ``CLAUDE.md`` documents the files reachable from its own
|
|
4
|
-
directory in a markdown table whose first column names each file in backticks
|
|
5
|
-
|
|
4
|
+
directory in a markdown table whose first column names each file in backticks,
|
|
5
|
+
and shows run commands inside fenced code blocks that invoke those files. When a
|
|
6
|
+
first-column cell, or an interpreter invocation inside a run-command fence
|
|
7
|
+
(``python script.py``), names a bare filename that exists nowhere under the scan
|
|
6
8
|
root (the CLAUDE.md directory's parent, covering the directory, its
|
|
7
|
-
subdirectories, and its siblings), the
|
|
8
|
-
|
|
9
|
-
extensions that mark a cell as a file
|
|
10
|
-
scopes a prose region to one section,
|
|
11
|
-
|
|
12
|
-
subtree
|
|
9
|
+
subdirectories, and its siblings), the doc points a reader at a file that is not
|
|
10
|
+
there. This module holds the patterns that find those cells and run-command
|
|
11
|
+
invocations, the filename extensions that mark a cell or invocation as a file
|
|
12
|
+
reference, the region-boundary marker that scopes a prose region to one section,
|
|
13
|
+
the relative-path marker that exempts a cross-directory table block, the
|
|
14
|
+
directory names the subtree walk prunes, the subtree scan budget, and the
|
|
15
|
+
block-message text the hook emits.
|
|
13
16
|
"""
|
|
14
17
|
|
|
15
18
|
import re
|
|
@@ -22,7 +25,9 @@ __all__ = [
|
|
|
22
25
|
"SEPARATOR_CELL_PATTERN",
|
|
23
26
|
"REGION_BOUNDARY_PATTERN",
|
|
24
27
|
"RELATIVE_PATH_SOURCE_PATTERN",
|
|
28
|
+
"RUN_COMMAND_SCRIPT_PATTERN",
|
|
25
29
|
"ALL_REFERENCED_FILE_EXTENSIONS",
|
|
30
|
+
"ALL_RUN_COMMAND_SCRIPT_EXTENSIONS",
|
|
26
31
|
"ALL_NOISE_DIRECTORY_NAMES",
|
|
27
32
|
"MAX_SUBTREE_FILES_SCANNED",
|
|
28
33
|
"MAX_ORPHAN_FILE_ISSUES",
|
|
@@ -45,6 +50,13 @@ REGION_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"^\s*#")
|
|
|
45
50
|
|
|
46
51
|
RELATIVE_PATH_SOURCE_PATTERN: re.Pattern[str] = re.compile(r"\.\.[\\/]")
|
|
47
52
|
|
|
53
|
+
RUN_COMMAND_SCRIPT_PATTERN: re.Pattern[str] = re.compile(
|
|
54
|
+
r"(?<![\w.])(?:python(?:\.exe)?|python3|node|pwsh|powershell(?:\.exe)?|bash|sh|ruby|perl)"
|
|
55
|
+
r"\b(?:\s+-\S+(?:\s+[\w./\\][\w./\\-]*(?=\s+\S))?)*"
|
|
56
|
+
r"\s+[\"']?"
|
|
57
|
+
r"([\w./\\-]+\.(?:py|mjs|js|ts|ps1|sh|rb|pl))\b"
|
|
58
|
+
)
|
|
59
|
+
|
|
48
60
|
ALL_REFERENCED_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
49
61
|
{
|
|
50
62
|
".py",
|
|
@@ -66,6 +78,19 @@ ALL_REFERENCED_FILE_EXTENSIONS: frozenset[str] = frozenset(
|
|
|
66
78
|
}
|
|
67
79
|
)
|
|
68
80
|
|
|
81
|
+
ALL_RUN_COMMAND_SCRIPT_EXTENSIONS: frozenset[str] = frozenset(
|
|
82
|
+
{
|
|
83
|
+
".py",
|
|
84
|
+
".mjs",
|
|
85
|
+
".js",
|
|
86
|
+
".ts",
|
|
87
|
+
".ps1",
|
|
88
|
+
".sh",
|
|
89
|
+
".rb",
|
|
90
|
+
".pl",
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
|
|
69
94
|
ALL_NOISE_DIRECTORY_NAMES: frozenset[str] = frozenset(
|
|
70
95
|
{
|
|
71
96
|
".git",
|
|
@@ -81,27 +106,30 @@ MAX_SUBTREE_FILES_SCANNED: int = 5000
|
|
|
81
106
|
MAX_ORPHAN_FILE_ISSUES: int = 20
|
|
82
107
|
|
|
83
108
|
ORPHAN_FILE_MESSAGE_TEMPLATE: str = (
|
|
84
|
-
"CLAUDE.md
|
|
85
|
-
"{missing}. A per-directory CLAUDE.md
|
|
86
|
-
"subtree
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"directory
|
|
109
|
+
"CLAUDE.md references files that exist nowhere under {directory}: "
|
|
110
|
+
"{missing}. A per-directory CLAUDE.md names files in its own directory "
|
|
111
|
+
"subtree, both in its table cells and in the run commands its fenced code "
|
|
112
|
+
"blocks show; a cell or a run command naming a file absent from that subtree "
|
|
113
|
+
"points a reader at something that is not there. Drop the row or run command, "
|
|
114
|
+
"or correct it to name a file that exists in this directory, a subdirectory of "
|
|
115
|
+
"it, or a sibling directory under its parent."
|
|
90
116
|
)
|
|
91
117
|
|
|
92
118
|
ORPHAN_FILE_SYSTEM_MESSAGE: str = (
|
|
93
|
-
"CLAUDE.md
|
|
94
|
-
"drop the row or name an existing file"
|
|
119
|
+
"CLAUDE.md names a file that does not exist in its directory subtree - "
|
|
120
|
+
"drop the row or run command, or name an existing file"
|
|
95
121
|
)
|
|
96
122
|
|
|
97
123
|
ORPHAN_FILE_ADDITIONAL_CONTEXT: str = (
|
|
98
124
|
"Each first-column table cell wrapped in backticks that ends in a known file "
|
|
99
|
-
"extension
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
125
|
+
"extension, and each script an interpreter invocation inside a fenced run "
|
|
126
|
+
"command names (such as 'python script.py'), must name a file present under "
|
|
127
|
+
"the scan root: this CLAUDE.md's own directory, a subdirectory of it, or a "
|
|
128
|
+
"sibling directory under its parent. Cells holding a path with a slash, a "
|
|
129
|
+
"subdirectory ending in '/', or a slash-command are out of scope. A table "
|
|
130
|
+
"whose own block names an explicit relative-path source (a '../' token) "
|
|
131
|
+
"documents files outside the subtree and is out of scope. For each missing "
|
|
132
|
+
"file:\n"
|
|
133
|
+
" - delete the table row or the run command, or\n"
|
|
134
|
+
" - rename it to an existing file under the scan root."
|
|
107
135
|
)
|
|
@@ -13,7 +13,7 @@ ALL_PYTHON_TOKENIZE_FAILURE_EXCEPTIONS: tuple[type[BaseException], ...] = (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
ALL_PYTHON_EXTENSIONS = {".py"}
|
|
16
|
-
ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx"}
|
|
16
|
+
ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".mts", ".cts"}
|
|
17
17
|
ALL_CODE_EXTENSIONS = ALL_PYTHON_EXTENSIONS | ALL_JAVASCRIPT_EXTENSIONS
|
|
18
18
|
|
|
19
19
|
ALL_TEST_PATH_PATTERNS = {"test_", "_test.", ".test.", ".spec.", "/tests/", "\\tests\\", "/tests.py", "\\tests.py"}
|
|
@@ -119,6 +119,46 @@ LOGGING_FSTRING_PATTERN = re.compile(
|
|
|
119
119
|
r'|(?:logger|logging|log)\.(?:debug|info|warning|error|critical|exception))'
|
|
120
120
|
r'\s*\(\s*(?:[rR][fF]|[fF][rR]?)["\']'
|
|
121
121
|
)
|
|
122
|
+
LOGGING_PRINTF_TOKEN_PATTERN: re.Pattern[str] = re.compile(
|
|
123
|
+
r"(?<!%)%[#0\- +]?[0-9.*]*[sdrixfgeEcoX](?![a-zA-Z])"
|
|
124
|
+
)
|
|
125
|
+
MINIMUM_FORMAT_LOGGER_ARGUMENT_COUNT = 2
|
|
126
|
+
SPAWN_AGENT_WITH_JSDOC_PATTERN: re.Pattern[str] = re.compile(
|
|
127
|
+
r"/\*\*(?P<jsdoc>(?:(?!\*/).)*?)\*/\s*"
|
|
128
|
+
r"(?:async\s+)?function\s+spawn(?P<role>\w+?)Agent\s*\(",
|
|
129
|
+
re.DOTALL,
|
|
130
|
+
)
|
|
131
|
+
RESUME_TASK_ENUMERATION_PATTERN: re.Pattern[str] = re.compile(
|
|
132
|
+
r"(?<![A-Za-z])resume\s*\((?P<enumeration>[^)]*?)\)",
|
|
133
|
+
re.DOTALL,
|
|
134
|
+
)
|
|
135
|
+
TASK_DISPATCH_NAME_PATTERN: re.Pattern[str] = re.compile(
|
|
136
|
+
r"""(?<![A-Za-z0-9_])task\s*===\s*['"](?P<task>[a-z0-9-]+)['"]"""
|
|
137
|
+
)
|
|
138
|
+
ENUMERATION_LIST_ITEM_SEPARATOR_PATTERN: re.Pattern[str] = re.compile(
|
|
139
|
+
r"\s*,\s*|\s+and\s+"
|
|
140
|
+
)
|
|
141
|
+
ENUMERATION_LEADING_CONJUNCTION_PATTERN: re.Pattern[str] = re.compile(
|
|
142
|
+
r"^and\s+"
|
|
143
|
+
)
|
|
144
|
+
ALL_JAVASCRIPT_STRING_DELIMITERS: frozenset[str] = frozenset({"'", '"', "`"})
|
|
145
|
+
JAVASCRIPT_STRING_ESCAPE_CHARACTER: str = "\\"
|
|
146
|
+
JAVASCRIPT_LINE_COMMENT_OPENER: str = "//"
|
|
147
|
+
JAVASCRIPT_BLOCK_COMMENT_OPENER: str = "/*"
|
|
148
|
+
JAVASCRIPT_BLOCK_COMMENT_CLOSER: str = "*/"
|
|
149
|
+
JAVASCRIPT_REGEX_DELIMITER: str = "/"
|
|
150
|
+
ALL_JAVASCRIPT_REGEX_PRECEDING_CHARACTERS: frozenset[str] = frozenset(
|
|
151
|
+
{"(", ",", "=", ":", "[", "{", "}", ";", "!", "&", "|", "?", "+", "-", "*", "%", "<", ">", "~", "^", "\n"}
|
|
152
|
+
)
|
|
153
|
+
ALL_JAVASCRIPT_REGEX_PRECEDING_KEYWORDS: frozenset[str] = frozenset(
|
|
154
|
+
{"return", "typeof", "case", "in", "of", "do", "else", "void", "delete", "instanceof", "new", "yield", "await", "throw"}
|
|
155
|
+
)
|
|
156
|
+
ENUMERATION_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
157
|
+
r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
|
158
|
+
)
|
|
159
|
+
HYPHENATED_TASK_ITEM_PATTERN: re.Pattern[str] = re.compile(
|
|
160
|
+
r"^[a-z0-9]+(?:-[a-z0-9]+)+$"
|
|
161
|
+
)
|
|
122
162
|
ALL_BUILTIN_DICT_METHOD_NAMES: frozenset[str] = frozenset({
|
|
123
163
|
"get", "items", "keys", "values", "update", "pop",
|
|
124
164
|
"setdefault", "copy", "clear",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The gate denies an ``Agent`` spawn whose ``subagent_type`` is ``code-verifier``
|
|
4
4
|
when the branch carries a merge conflict against its base ref or a CODE_RULES
|
|
5
|
-
violation on a line added in the
|
|
5
|
+
violation on a line added in the working tree since the merge base (committed on
|
|
6
|
+
the branch or uncommitted). It runs two
|
|
6
7
|
pre-flight checks before the expensive verification spawn and addresses its
|
|
7
8
|
deny reason to the spawning agent so that agent fixes the named issues and
|
|
8
9
|
re-spawns. Every literal the hook body reads lives here; the hook imports
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
"""Constants for the duplicate-function-body
|
|
1
|
+
"""Constants for the duplicate-function-body scans in ``code_rules_enforcer``.
|
|
2
2
|
|
|
3
|
-
The blocking scan flags a top-level function whose body is
|
|
4
|
-
to a top-level function already defined in a sibling
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The cross-file blocking scan flags a top-level function whose body is
|
|
4
|
+
structurally identical to a top-level function already defined in a sibling
|
|
5
|
+
``.py`` module in the same directory. The same-file blocking scan flags a
|
|
6
|
+
top-level function whose body appears verbatim as a contiguous statement block
|
|
7
|
+
inside another function in the same module. Both catch the Reuse-before-create /
|
|
8
|
+
DRY violation where a block of logic is copied instead of called from one shared
|
|
9
|
+
home, so a fix that lands in one copy leaves the other carrying the bug.
|
|
7
10
|
|
|
8
11
|
The ``CROSS_SKILL_*`` and ``SKILL*`` constants feed the non-blocking companion
|
|
9
12
|
advisory: a helper copied between two skills' ``scripts`` directories, where a
|
|
@@ -12,6 +15,7 @@ skill on stderr rather than denying the write.
|
|
|
12
15
|
"""
|
|
13
16
|
|
|
14
17
|
MINIMUM_DUPLICATE_BODY_STATEMENTS: int = 3
|
|
18
|
+
MINIMUM_INLINE_DUPLICATE_BODY_STATEMENTS: int = 1
|
|
15
19
|
MAX_DUPLICATE_BODY_ISSUES: int = 25
|
|
16
20
|
DUNDER_INIT_FILENAME: str = "__init__.py"
|
|
17
21
|
PYTHON_SOURCE_SUFFIX: str = ".py"
|
|
@@ -20,6 +24,18 @@ DUPLICATE_BODY_GUIDANCE: str = (
|
|
|
20
24
|
"extract a single shared helper (for example in hooks_constants/) and "
|
|
21
25
|
"import it from both modules instead of copying it (Reuse before create / DRY)"
|
|
22
26
|
)
|
|
27
|
+
SAME_FILE_INLINE_DUPLICATE_GUIDANCE: str = (
|
|
28
|
+
"this function body is also present inline as a contiguous statement block "
|
|
29
|
+
"inside another function in the same module; call this helper from that "
|
|
30
|
+
"function instead of repeating the block, so a single helper backs both call "
|
|
31
|
+
"sites and a fix cannot land in one copy while the other keeps the bug "
|
|
32
|
+
"(Reuse before create / DRY)"
|
|
33
|
+
)
|
|
34
|
+
SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE: str = (
|
|
35
|
+
"(inline duplicate body spans: helper at line {helper_start} spanning "
|
|
36
|
+
"{helper_length} lines, enclosing at line {enclosing_start} spanning "
|
|
37
|
+
"{enclosing_length} lines)"
|
|
38
|
+
)
|
|
23
39
|
|
|
24
40
|
SKILLS_DIRECTORY_NAME: str = "skills"
|
|
25
41
|
SKILL_SCRIPTS_DIRECTORY_NAME: str = "scripts"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Configuration constants for the duplicate_rmtree_helper_blocker PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
PYTHON_FILE_EXTENSION: str = ".py"
|
|
6
|
+
|
|
7
|
+
HELPER_DEFINITION_PATTERN: re.Pattern[str] = re.compile(
|
|
8
|
+
r"^[ \t]*def[ \t]+(?:_strip_read_only_and_retry|_force_remove_tree|force_rmtree)[ \t]*\(",
|
|
9
|
+
re.MULTILINE,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
TRIPLE_QUOTED_STRING_PATTERN: re.Pattern[str] = re.compile(
|
|
13
|
+
r'"""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\'',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
ALL_EXEMPT_PATH_FRAGMENTS: tuple[str, ...] = (
|
|
17
|
+
"windows_rmtree_blocker.py",
|
|
18
|
+
"duplicate_rmtree_helper_blocker.py",
|
|
19
|
+
"windows_safe_rmtree.py",
|
|
20
|
+
"windows_filesystem.py",
|
|
21
|
+
"session_env_cleanup.py",
|
|
22
|
+
"_md_to_html_blocker_test_support.py",
|
|
23
|
+
"teardown_worktrees.py",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
ALL_EXEMPT_TEST_FILE_PREFIXES: tuple[str, ...] = ("test_",)
|
|
27
|
+
ALL_EXEMPT_TEST_FILE_SUFFIXES: tuple[str, ...] = ("_test.py",)
|
|
@@ -15,6 +15,7 @@ __all__ = [
|
|
|
15
15
|
"REASON_KEY",
|
|
16
16
|
"HOOK_EVENT_NAME",
|
|
17
17
|
"EMPTY_REASON_BLOCK_FALLBACK",
|
|
18
|
+
"BLOCKING_CRASH_DENY_REASON",
|
|
18
19
|
"PLUGIN_ROOT_PLACEHOLDER",
|
|
19
20
|
"PostHostedHookEntry",
|
|
20
21
|
"ALL_POST_HOSTED_HOOK_ENTRIES",
|
|
@@ -25,6 +26,7 @@ DECISION_KEY = "decision"
|
|
|
25
26
|
REASON_KEY = "reason"
|
|
26
27
|
HOOK_EVENT_NAME = "PostToolUse"
|
|
27
28
|
EMPTY_REASON_BLOCK_FALLBACK = "[dispatcher] hook blocked with no reason — write blocked"
|
|
29
|
+
BLOCKING_CRASH_DENY_REASON = "[dispatcher] hook crash in blocking hook — write blocked for safety"
|
|
28
30
|
|
|
29
31
|
PLUGIN_ROOT_PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}"
|
|
30
32
|
|