claude-dev-env 2.5.0 → 2.7.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.
Files changed (153) hide show
  1. package/CLAUDE.md +20 -57
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
  3. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  12. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  13. package/_shared/pr-loop/worker-spawn.md +3 -1
  14. package/agents/CLAUDE.md +1 -1
  15. package/agents/code-verifier.md +36 -7
  16. package/bin/codex-compat.mjs +104 -0
  17. package/bin/codex-compat.test.mjs +51 -0
  18. package/codex-capability-map.json +13 -0
  19. package/docs/CODE_RULES.md +2 -0
  20. package/docs/codex-compatibility.md +25 -0
  21. package/docs/nas-ssh-invocation.md +96 -12
  22. package/docs/references/code-review-enforcement.md +31 -6
  23. package/hooks/blocking/CLAUDE.md +3 -0
  24. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
  25. package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
  26. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  27. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  28. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  29. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  30. package/hooks/blocking/state_description_blocker.py +1 -1
  31. package/hooks/blocking/stop_dispatcher.py +1 -1
  32. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  33. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  34. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  35. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  36. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  37. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
  38. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  39. package/hooks/git-hooks/CLAUDE.md +3 -0
  40. package/hooks/git-hooks/conftest.py +30 -0
  41. package/hooks/git-hooks/gate_utils.py +2 -2
  42. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  43. package/hooks/git-hooks/pre_push.py +75 -4
  44. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  45. package/hooks/git-hooks/test_config.py +0 -15
  46. package/hooks/git-hooks/test_gate_utils.py +3 -15
  47. package/hooks/git-hooks/test_pre_commit.py +1 -15
  48. package/hooks/git-hooks/test_pre_push.py +236 -27
  49. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  50. package/hooks/hooks.json +0 -12
  51. package/hooks/hooks_constants/CLAUDE.md +5 -1
  52. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  53. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  54. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  55. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  56. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  57. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  58. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  59. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  60. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  61. package/package.json +4 -2
  62. package/rules/CLAUDE.md +17 -23
  63. package/rules/agent-spawn-protocol.md +6 -6
  64. package/rules/anti-corollary-tests.md +1 -1
  65. package/rules/bdd.md +1 -1
  66. package/rules/cleanup-temp-files.md +10 -4
  67. package/rules/code-standards.md +7 -0
  68. package/rules/conservative-action.md +1 -5
  69. package/rules/context7.md +0 -4
  70. package/rules/destructive-commands.md +47 -0
  71. package/rules/doc-inventory-integrity.md +48 -0
  72. package/rules/doc-prose-cuts.md +58 -0
  73. package/rules/docstring-prose-matches-implementation.md +10 -2
  74. package/rules/durable-post-artifacts.md +0 -4
  75. package/rules/eli11-replies.md +31 -0
  76. package/rules/explore-thoroughly.md +4 -4
  77. package/rules/falsify-before-green.md +68 -0
  78. package/rules/file-global-constants.md +1 -1
  79. package/rules/filesystem-search.md +51 -0
  80. package/rules/gh-cli-conventions.md +27 -0
  81. package/rules/git-workflow.md +26 -0
  82. package/rules/hedging-claims.md +9 -0
  83. package/rules/long-horizon-autonomy.md +0 -4
  84. package/rules/measurement-denominators.md +48 -0
  85. package/rules/nas-ssh-invocation.md +23 -5
  86. package/rules/parallel-tools.md +2 -2
  87. package/rules/plain-illustrative-docstrings.md +3 -7
  88. package/rules/plain-language.md +2 -0
  89. package/rules/proof-of-work-pr-comments.md +0 -4
  90. package/rules/re-stage-before-commit.md +2 -0
  91. package/rules/research-mode.md +10 -0
  92. package/rules/shell-invocation.md +21 -0
  93. package/rules/testing.md +4 -0
  94. package/rules/verified-commit-gate-skip.md +3 -27
  95. package/rules/verify-before-asking.md +5 -0
  96. package/rules/windows-filesystem-safe.md +1 -1
  97. package/rules/workers-done-before-complete.md +4 -0
  98. package/scripts/CLAUDE.md +3 -3
  99. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  100. package/scripts/codex_capability_bridge.py +171 -0
  101. package/scripts/codex_compat_materializer.py +1087 -0
  102. package/scripts/codex_compat_watcher.py +502 -0
  103. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  104. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  105. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +79 -13
  106. package/scripts/grok_headless_runner.py +213 -16
  107. package/scripts/invoke_code_review.py +11 -4
  108. package/scripts/resolve_worker_spawn.py +56 -10
  109. package/scripts/spawn_grok_batch.py +43 -22
  110. package/scripts/sync_to_cursor/rules.py +0 -10
  111. package/scripts/test_grok_headless_runner.py +592 -10
  112. package/scripts/test_invoke_code_review.py +143 -0
  113. package/scripts/test_invoke_code_review_chain.py +1 -1
  114. package/scripts/test_invoke_code_review_contract.py +1 -1
  115. package/scripts/test_resolve_worker_spawn.py +179 -15
  116. package/scripts/test_spawn_grok_batch.py +225 -22
  117. package/scripts/tests/test_code_review_constants.py +80 -0
  118. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  119. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  120. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  121. package/scripts/tests/test_sync_to_cursor.py +0 -1
  122. package/skills/autoconverge/workflow/converge.contract.test.mjs +28 -6
  123. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +73 -0
  124. package/skills/autoconverge/workflow/converge.mjs +64 -15
  125. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  126. package/skills/condensing-instructions/SKILL.md +42 -51
  127. package/skills/fresh-branch/CLAUDE.md +1 -1
  128. package/skills/fresh-branch/SKILL.md +5 -6
  129. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  130. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  131. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  132. package/skills/grok-spawn/SKILL.md +5 -3
  133. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  134. package/skills/orchestrator/SKILL.md +23 -9
  135. package/skills/orchestrator-refresh/SKILL.md +20 -1
  136. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  137. package/skills/session-log/SKILL.md +1 -1
  138. package/rules/claude-md-orphan-file.md +0 -28
  139. package/rules/cleanup-command-forms.md +0 -23
  140. package/rules/code-reviews.md +0 -11
  141. package/rules/env-var-table-code-drift.md +0 -10
  142. package/rules/gh-body-file.md +0 -5
  143. package/rules/gh-paginate.md +0 -3
  144. package/rules/hook-prose-matches-detector.md +0 -15
  145. package/rules/no-historical-clutter.md +0 -26
  146. package/rules/no-inline-destructive-literals.md +0 -9
  147. package/rules/no-justification-noise.md +0 -61
  148. package/rules/package-inventory-stale-entry.md +0 -25
  149. package/rules/right-sized-engineering.md +0 -28
  150. package/rules/self-contained-docs.md +0 -17
  151. package/rules/shell-invocation-policy.md +0 -5
  152. package/rules/state-what-is.md +0 -25
  153. package/rules/tdd.md +0 -7
package/hooks/hooks.json CHANGED
@@ -202,18 +202,6 @@
202
202
  ]
203
203
  }
204
204
  ],
205
- "SubagentStop": [
206
- {
207
- "matcher": "",
208
- "hooks": [
209
- {
210
- "type": "command",
211
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/verifier_verdict_minter.py",
212
- "timeout": 30
213
- }
214
- ]
215
- }
216
- ],
217
205
  "SessionEnd": [
218
206
  {
219
207
  "matcher": "",
@@ -31,6 +31,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
31
31
  | `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
32
32
  | `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 |
33
33
  | `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
34
+ | `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (word cap, enforced word floor, bullet cap, lead-line window, per-line word cap, over-packed line cap), the `Long form:` escape prefix, the bullet / numbered-step / table-row / link-target / word patterns, the imperative instruction verbs, and the user-facing notice for the ELI11 reply-shape Stop hook |
34
35
  | `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
35
36
  | `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
36
37
  | `harness_scratchpad_constants.py` | Fixed path components (`claude` / `claude-` user-directory name and prefix, `scratchpad` leaf name), the PreToolUse session-id payload key, and the `CLAUDE_CODE_SESSION_ID` environment variable name that the code-rules and TDD gates use to recognize an existing harness session scratchpad directory |
@@ -45,7 +46,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
45
46
  | `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 |
46
47
  | `mypy_integration_constants.py` | Path markers (``.git``, ``.py``, ``pyproject.toml``) for mypy project-root resolution |
47
48
  | `mypy_validator_cache_constants.py` | Cache paths and tunables for the mypy_validator per-session caches |
48
- | `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 |
49
+ | `nas_ssh_binary_enforcer_constants.py` | Bash tool name, ssh-family basenames, OpenSSH binary path suffixes, and the batch-mode pattern for the NAS ssh binary enforcer; segment helpers come from `shell_command_segments.py` |
49
50
  | `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
50
51
  | `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
51
52
  | `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 |
@@ -71,6 +72,8 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
71
72
  | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
72
73
  | `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
73
74
  | `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
75
+ | `shell_command_segments.py` | Shell tokenization helpers shared by the segment-walking blockers: segment splitting on control operators, a segment's effective leading program, and a token's basename |
76
+ | `shell_substitution_blocker_constants.py` | Tool name, payload keys, the four substitution-detection patterns, the deny decision shape, and the corrective message for the shell-substitution blocker |
74
77
  | `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
75
78
  | `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 |
76
79
  | `stop_dispatcher_constants.py` | Ordered hosted-hook roster and Stop block payload field names for the Stop-chain dispatcher |
@@ -81,6 +84,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
81
84
  | `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 |
82
85
  | `test_layout_constants.py` | The named constants for the test-layout write-time checks |
83
86
  | `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 |
87
+ | `unscoped_search_blocker_constants.py` | Tool names, `find` and listing program basenames, Get-ChildItem option sets, unscoped-root patterns, the flag/value token stride, and the deny message for the unscoped-search blocker |
84
88
  | `unused_module_import_constants.py` | Patterns for detecting unused module-level imports |
85
89
  | `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 |
86
90
  | `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
@@ -56,6 +56,10 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
56
56
  BashHostedHookEntry("blocking/es_exe_path_rewriter.py", ALL_BASH_ONLY_TOOL_NAMES),
57
57
  BashHostedHookEntry("blocking/destructive_command_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
58
58
  BashHostedHookEntry("blocking/gh_body_arg_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
59
+ BashHostedHookEntry("blocking/shell_substitution_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
60
+ BashHostedHookEntry(
61
+ "blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
62
+ ),
59
63
  BashHostedHookEntry("blocking/nas_ssh_binary_enforcer.py", ALL_BASH_ONLY_TOOL_NAMES),
60
64
  BashHostedHookEntry("blocking/volatile_path_in_post_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
61
65
  BashHostedHookEntry(
@@ -71,10 +75,6 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
71
75
  BashHostedHookEntry("blocking/convergence_gate_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
72
76
  BashHostedHookEntry("blocking/windows_rmtree_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
73
77
  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
78
  BashHostedHookEntry("blocking/code_review_push_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
79
79
  BashHostedHookEntry(
80
80
  "blocking/code_review_pr_create_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
@@ -0,0 +1,101 @@
1
+ """Tunables for the eli11_reply_enforcer Stop hook.
2
+
3
+ ::
4
+
5
+ NUMBERED_STEP_PATTERN
6
+ ok: "1. Run the script" flag: "1.5% of hosts still fail."
7
+ ALL_IMPERATIVE_OBJECT_LEAD_WORDS
8
+ ok: "Run the migration" flag: "Run time stays under a second"
9
+
10
+ Holds the reply-shape thresholds the `eli11-replies` rule names, the compiled
11
+ patterns that read a reply's structure, the closed sets naming an instruction
12
+ verb and its object, and the notice the hook shows when it blocks.
13
+ """
14
+
15
+ import re
16
+
17
+ __all__ = [
18
+ "ACTION_FIRST_LEAD_LINE_COUNT",
19
+ "ALL_IMPERATIVE_INSTRUCTION_VERBS",
20
+ "ALL_IMPERATIVE_OBJECT_LEAD_WORDS",
21
+ "ALPHABETIC_WORD_PATTERN",
22
+ "BULLET_LINE_PATTERN",
23
+ "COUNTABLE_WORD_PATTERN",
24
+ "IMPERATIVE_OBJECT_TOKEN_PATTERN",
25
+ "LINK_TARGET_PATTERN",
26
+ "LIST_MARKER_PREFIX_PATTERN",
27
+ "LONG_FORM_ESCAPE_PREFIX",
28
+ "MARKDOWN_LEAD_MARKER_PATTERN",
29
+ "MAXIMUM_BULLET_LINE_COUNT",
30
+ "MAXIMUM_OVERPACKED_LINE_COUNT",
31
+ "MAXIMUM_REPLY_WORD_COUNT",
32
+ "MAXIMUM_WORDS_PER_LINE",
33
+ "MINIMUM_ENFORCED_WORD_COUNT",
34
+ "NUMBERED_STEP_PATTERN",
35
+ "TABLE_ROW_PATTERN",
36
+ "TARGET_BULLET_LINE_COUNT",
37
+ "USER_FACING_ELI11_NOTICE",
38
+ "VIOLATION_SEPARATOR",
39
+ ]
40
+
41
+ MAXIMUM_REPLY_WORD_COUNT = 120
42
+ MINIMUM_ENFORCED_WORD_COUNT = 60
43
+ MAXIMUM_BULLET_LINE_COUNT = 6
44
+ TARGET_BULLET_LINE_COUNT = 3
45
+ ACTION_FIRST_LEAD_LINE_COUNT = 6
46
+ MAXIMUM_WORDS_PER_LINE = 20
47
+ MAXIMUM_OVERPACKED_LINE_COUNT = 2
48
+
49
+ LONG_FORM_ESCAPE_PREFIX = "long form:"
50
+ VIOLATION_SEPARATOR = "; "
51
+ USER_FACING_ELI11_NOTICE = "Agent wrote a long reply - rewriting it short..."
52
+
53
+ TABLE_ROW_PATTERN = re.compile(r"^[ \t]*\|.*\|[ \t]*$", re.MULTILINE)
54
+ LINK_TARGET_PATTERN = re.compile(r"(?:https?://|www\.)\S+", re.IGNORECASE)
55
+ COUNTABLE_WORD_PATTERN = re.compile(r"\S*[A-Za-z0-9]\S*")
56
+ ALPHABETIC_WORD_PATTERN = re.compile(r"[A-Za-z][A-Za-z'’-]*")
57
+ BULLET_LINE_PATTERN = re.compile(r"^[ \t]*[-*+][ \t]+\S")
58
+ NUMBERED_STEP_PATTERN = re.compile(r"^[ \t]*(?:\*\*)?\d{1,2}[.)](?:\*\*)?[ \t]+\S")
59
+ LIST_MARKER_PREFIX_PATTERN = re.compile(
60
+ r"^[ \t]*(?:[-*+][ \t]+|(?:\*\*)?\d{1,2}[.)][ \t]*)?(?:\*\*)?"
61
+ )
62
+ MARKDOWN_LEAD_MARKER_PATTERN = re.compile(r"^(?:[ \t]*(?:>+|#{1,6}|\*\*|__|\*|_))+[ \t]*")
63
+
64
+ ALL_IMPERATIVE_INSTRUCTION_VERBS = (
65
+ "click",
66
+ "copy",
67
+ "delete",
68
+ "do",
69
+ "download",
70
+ "install",
71
+ "merge",
72
+ "open",
73
+ "paste",
74
+ "restart",
75
+ "run",
76
+ "save",
77
+ )
78
+
79
+ ALL_IMPERATIVE_OBJECT_LEAD_WORDS = (
80
+ "a",
81
+ "all",
82
+ "an",
83
+ "any",
84
+ "both",
85
+ "each",
86
+ "every",
87
+ "into",
88
+ "it",
89
+ "my",
90
+ "our",
91
+ "that",
92
+ "the",
93
+ "them",
94
+ "these",
95
+ "this",
96
+ "those",
97
+ "to",
98
+ "your",
99
+ )
100
+
101
+ IMPERATIVE_OBJECT_TOKEN_PATTERN = re.compile(r"^\d|[`/\\]|\.[A-Za-z]{1,4}$")
@@ -1,9 +1,8 @@
1
1
  """Constants for the NAS ssh binary enforcer hook.
2
2
 
3
3
  Holds the Bash tool name, the ssh-family command basenames, the Windows OpenSSH
4
- binary path suffixes, the launcher-wrapper set, the shell control-operator tokens
5
- and their split pattern, the leading-assignment and launcher-duration patterns,
6
- and the batch-mode pattern.
4
+ binary path suffixes, and the batch-mode pattern. Segment splitting and
5
+ leading-program resolution come from ``shell_command_segments``.
7
6
  """
8
7
 
9
8
  import re
@@ -18,9 +17,4 @@ ALL_OPENSSH_BINARY_PATH_SUFFIXES = (
18
17
  "/openssh/scp.exe",
19
18
  "/openssh/sftp.exe",
20
19
  )
21
- ALL_LAUNCHER_WRAPPER_COMMANDS = frozenset({"timeout", "nohup", "nice", "stdbuf", "setsid", "env"})
22
- ALL_SHELL_CONTROL_OPERATOR_TOKENS = frozenset({"&&", "||", ";", "|", "&", "|&"})
23
- CONTROL_OPERATOR_SPLIT_PATTERN = re.compile(r"(&&|\|\||;|\|&|\||(?<!>)&(?!>))")
24
- LEADING_ASSIGNMENT_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
25
- LAUNCHER_DURATION_PATTERN = re.compile(r"^\d+[a-z]*$", re.IGNORECASE)
26
20
  BATCH_MODE_PATTERN = re.compile(r"batchmode\s*=\s*yes", re.IGNORECASE)
@@ -0,0 +1,82 @@
1
+ """Shared shell-command segment helpers for Bash PreToolUse blockers.
2
+
3
+ Tokenizes a command into simple-command segments on control operators and finds
4
+ each segment's effective leading program after env assignments and launcher
5
+ wrappers. NAS ssh enforcement and unscoped-search blocking both need this
6
+ shape, so one module owns it.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+
13
+ __all__ = [
14
+ "ALL_LAUNCHER_WRAPPER_COMMANDS",
15
+ "ALL_SHELL_CONTROL_OPERATOR_TOKENS",
16
+ "CONTROL_OPERATOR_SPLIT_PATTERN",
17
+ "LEADING_ASSIGNMENT_PATTERN",
18
+ "LAUNCHER_DURATION_PATTERN",
19
+ "token_basename",
20
+ "split_into_segments",
21
+ "effective_leading_program",
22
+ ]
23
+
24
+ ALL_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
25
+ {"timeout", "nohup", "nice", "stdbuf", "setsid", "env"}
26
+ )
27
+ ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset(
28
+ {"&&", "||", ";", "|", "&", "|&"}
29
+ )
30
+ CONTROL_OPERATOR_SPLIT_PATTERN = re.compile(r"(&&|\|\||;|\|&|\||(?<!>)&(?!>))")
31
+ LEADING_ASSIGNMENT_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
32
+ LAUNCHER_DURATION_PATTERN = re.compile(r"^\d+[a-z]*$", re.IGNORECASE)
33
+
34
+
35
+ def token_basename(token: str) -> str:
36
+ """Return the lowercased basename of a path-or-command token."""
37
+ return token.replace("\\", "/").rsplit("/", 1)[-1].lower()
38
+
39
+
40
+ def split_into_segments(all_command_tokens: list[str]) -> list[list[str]]:
41
+ """Split tokens into simple-command segments on shell control operators.
42
+
43
+ Glued operators on a single token are exploded first, then the stream is
44
+ cut on ``&&`` / ``||`` / ``;`` / ``|`` / ``&`` / ``|&``.
45
+ """
46
+ all_exploded_tokens: list[str] = []
47
+ for each_token in all_command_tokens:
48
+ for each_fragment in CONTROL_OPERATOR_SPLIT_PATTERN.split(each_token):
49
+ if each_fragment:
50
+ all_exploded_tokens.append(each_fragment)
51
+ all_segments: list[list[str]] = []
52
+ current_segment: list[str] = []
53
+ for each_token in all_exploded_tokens:
54
+ if each_token in ALL_SHELL_CONTROL_OPERATOR_TOKENS:
55
+ all_segments.append(current_segment)
56
+ current_segment = []
57
+ continue
58
+ current_segment.append(each_token)
59
+ all_segments.append(current_segment)
60
+ return all_segments
61
+
62
+
63
+ def effective_leading_program(all_segment_tokens: list[str]) -> str | None:
64
+ """Return the effective program token after assignments and launcher wrappers.
65
+
66
+ Skips ``VAR=value`` prefixes and known launchers (``timeout``, ``env``, …)
67
+ plus their flags and duration arguments. Returns None when no program token
68
+ remains.
69
+ """
70
+ has_seen_launcher_wrapper = False
71
+ for each_token in all_segment_tokens:
72
+ if LEADING_ASSIGNMENT_PATTERN.match(each_token):
73
+ continue
74
+ if token_basename(each_token) in ALL_LAUNCHER_WRAPPER_COMMANDS:
75
+ has_seen_launcher_wrapper = True
76
+ continue
77
+ if has_seen_launcher_wrapper and (
78
+ each_token.startswith("-") or LAUNCHER_DURATION_PATTERN.match(each_token)
79
+ ):
80
+ continue
81
+ return each_token
82
+ return None
@@ -0,0 +1,67 @@
1
+ """Constants for the shell-substitution PreToolUse Bash blocker.
2
+
3
+ Holds the tool name, the payload keys, the four detection patterns, the deny
4
+ decision shape, and the corrective message. The hook imports these so no
5
+ single-use constant sits at its file scope.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ __all__ = [
13
+ "BASH_TOOL_NAME",
14
+ "TOOL_NAME_KEY",
15
+ "TOOL_INPUT_KEY",
16
+ "COMMAND_KEY",
17
+ "HOOK_SPECIFIC_OUTPUT_KEY",
18
+ "HOOK_EVENT_NAME_KEY",
19
+ "HOOK_EVENT_NAME",
20
+ "PERMISSION_DECISION_KEY",
21
+ "DENY_DECISION",
22
+ "PERMISSION_DECISION_REASON_KEY",
23
+ "DOLLAR_PAREN_PATTERN",
24
+ "EVEN_BACKSLASH_BACKTICK_PATTERN",
25
+ "PROCESS_SUBSTITUTION_PATTERN",
26
+ "SINGLE_QUOTED_RUN_PATTERN",
27
+ "STRIPPED_RUN_REPLACEMENT",
28
+ "CORRECTIVE_MESSAGE",
29
+ ]
30
+
31
+ BASH_TOOL_NAME = "Bash"
32
+
33
+ TOOL_NAME_KEY = "tool_name"
34
+ TOOL_INPUT_KEY = "tool_input"
35
+ COMMAND_KEY = "command"
36
+
37
+ HOOK_SPECIFIC_OUTPUT_KEY = "hookSpecificOutput"
38
+ HOOK_EVENT_NAME_KEY = "hookEventName"
39
+ HOOK_EVENT_NAME = "PreToolUse"
40
+ PERMISSION_DECISION_KEY = "permissionDecision"
41
+ DENY_DECISION = "deny"
42
+ PERMISSION_DECISION_REASON_KEY = "permissionDecisionReason"
43
+
44
+ DOLLAR_PAREN_PATTERN = re.compile(r"\$\((?!\()")
45
+ EVEN_BACKSLASH_BACKTICK_PATTERN = re.compile(r"(?<!\\)(?:\\\\)*`")
46
+ PROCESS_SUBSTITUTION_PATTERN = re.compile(r"[<>]\(")
47
+ SINGLE_QUOTED_RUN_PATTERN = re.compile(r"'[^']*'")
48
+ STRIPPED_RUN_REPLACEMENT = ""
49
+
50
+ CORRECTIVE_MESSAGE = (
51
+ "BLOCKED [shell-substitution]: command contains $(...), backtick, or "
52
+ "<(...)/>(...) process substitution, which forces a permission prompt "
53
+ "because the Claude Code allowlist matcher does not descend into "
54
+ "substitutions.\n\n"
55
+ "Fix: split into two Bash tool calls, or rewrite without substitution.\n"
56
+ ' cd X && echo "$(git rev-parse HEAD)"\n'
57
+ "becomes either two calls:\n"
58
+ " 1) cd X\n"
59
+ " 2) git rev-parse HEAD\n"
60
+ "or one substitution-free call:\n"
61
+ " git -C X rev-parse HEAD\n\n"
62
+ "Process substitution example:\n"
63
+ " diff <(cat a) <(cat b)\n"
64
+ "becomes two separate Bash calls to `cat a` and `cat b`, with the diff "
65
+ "performed on the captured outputs.\n\n"
66
+ "See `shell-invocation.md` in the rules directory for full guidance."
67
+ )
@@ -22,5 +22,6 @@ ALL_STOP_HOSTED_HOOK_PATHS: tuple[str, ...] = (
22
22
  "blocking/question_to_user_enforcer.py",
23
23
  "blocking/intent_only_ending_blocker.py",
24
24
  "blocking/session_handoff_blocker.py",
25
+ "blocking/eli11_reply_enforcer.py",
25
26
  "diagnostic/hook_log_stop_wrapper.py",
26
27
  )
@@ -25,6 +25,8 @@ _EXPECTED_BASH_ORDER = (
25
25
  "blocking/es_exe_path_rewriter.py",
26
26
  "blocking/destructive_command_blocker.py",
27
27
  "blocking/gh_body_arg_blocker.py",
28
+ "blocking/shell_substitution_blocker.py",
29
+ "blocking/unscoped_search_blocker.py",
28
30
  "blocking/nas_ssh_binary_enforcer.py",
29
31
  "blocking/volatile_path_in_post_blocker.py",
30
32
  "blocking/pii_prevention_blocker.py",
@@ -38,17 +40,14 @@ _EXPECTED_BASH_ORDER = (
38
40
  "blocking/convergence_gate_blocker.py",
39
41
  "blocking/windows_rmtree_blocker.py",
40
42
  "blocking/gh_pr_author_enforcer.py",
41
- "blocking/verified_commit_gate.py",
42
- "blocking/verdict_directory_write_blocker.py",
43
43
  "blocking/code_review_push_gate.py",
44
44
  "blocking/code_review_pr_create_gate.py",
45
45
  "blocking/code_review_stamp_directory_write_blocker.py",
46
46
  )
47
47
 
48
48
  _POWERSHELL_APPLICABLE = (
49
+ "blocking/unscoped_search_blocker.py",
49
50
  "blocking/pii_prevention_blocker.py",
50
- "blocking/verified_commit_gate.py",
51
- "blocking/verdict_directory_write_blocker.py",
52
51
  "blocking/code_review_push_gate.py",
53
52
  "blocking/code_review_pr_create_gate.py",
54
53
  "blocking/code_review_stamp_directory_write_blocker.py",
@@ -69,8 +68,8 @@ def test_every_hook_applies_to_the_bash_tool() -> None:
69
68
  assert BASH_TOOL_NAME in each_entry.applicable_tool_names
70
69
 
71
70
 
72
- def test_powershell_applicable_hooks_include_pii_and_verified_commit_pair() -> None:
73
- """PowerShell runs the PII gate plus the verified-commit pair."""
71
+ def test_powershell_applicable_hooks_include_the_shared_gates() -> None:
72
+ """PowerShell runs the unscoped-search and PII gates plus the review family."""
74
73
  powershell_hooks = tuple(
75
74
  each_entry.script_relative_path
76
75
  for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
@@ -16,6 +16,7 @@ _EXPECTED_STOP_ORDER = (
16
16
  "blocking/question_to_user_enforcer.py",
17
17
  "blocking/intent_only_ending_blocker.py",
18
18
  "blocking/session_handoff_blocker.py",
19
+ "blocking/eli11_reply_enforcer.py",
19
20
  "diagnostic/hook_log_stop_wrapper.py",
20
21
  )
21
22
 
@@ -0,0 +1,153 @@
1
+ """Constants for the unscoped-search PreToolUse Bash/PowerShell blocker.
2
+
3
+ Holds tool names, program basenames, Get-ChildItem option sets, the clustered
4
+ short-option pattern, unscoped root patterns (drive root, posix root alias, UNC
5
+ share root), the shared flag/value token stride, and the deny message. Find
6
+ global option flags reuse ``destructive_command_segment_constants`` so both find
7
+ parsers stay on one set.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ from hooks_constants.destructive_command_segment_constants import (
15
+ ALL_FIND_GLOBAL_OPTION_FLAGS_TAKING_A_VALUE,
16
+ ALL_FIND_GLOBAL_OPTION_FLAGS_WITHOUT_VALUE,
17
+ FIND_OPTIMIZATION_LEVEL_OPTION_PREFIX,
18
+ )
19
+
20
+ __all__ = [
21
+ "BASH_TOOL_NAME",
22
+ "POWERSHELL_TOOL_NAME",
23
+ "ALL_SUPPORTED_TOOL_NAMES",
24
+ "ALL_FIND_PROGRAM_BASENAMES",
25
+ "ALL_POWERSHELL_LISTING_PROGRAM_BASENAMES",
26
+ "ALL_UNIX_LS_PROGRAM_BASENAMES",
27
+ "ALL_LISTING_PROGRAM_BASENAMES",
28
+ "ALL_FIND_GLOBAL_OPTION_FLAGS_WITHOUT_VALUE",
29
+ "ALL_FIND_GLOBAL_OPTION_FLAGS_TAKING_A_VALUE",
30
+ "FIND_OPTIMIZATION_LEVEL_OPTION_PREFIX",
31
+ "FLAG_AND_VALUE_TOKEN_STRIDE",
32
+ "ALL_FIND_EXPRESSION_INTRODUCER_TOKENS",
33
+ "ALL_POWERSHELL_RECURSE_FLAGS",
34
+ "ALL_UNIX_LS_RECURSE_FLAGS",
35
+ "UNIX_LS_RECURSE_SHORT_OPTION_LETTER",
36
+ "CLUSTERED_SHORT_OPTION_PATTERN",
37
+ "ALL_POWERSHELL_RECURSE_FLAG_PREFIXES",
38
+ "ALL_POWERSHELL_PATH_FLAGS",
39
+ "ALL_POWERSHELL_PATH_FLAG_PREFIXES",
40
+ "ALL_TRUTHY_RECURSE_COLON_VALUES",
41
+ "ALL_STRING_EXECUTING_SHELL_BASENAMES",
42
+ "ALL_STRING_EXEC_COMMAND_FLAGS",
43
+ "ALL_UNSCOPED_HOME_LITERALS_CASEFOLD",
44
+ "GIT_BASH_DRIVE_ROOT_PATTERN",
45
+ "WINDOWS_DRIVE_ROOT_PATTERN",
46
+ "POSIX_ROOT_ALIAS_PATTERN",
47
+ "UNC_SHARE_ROOT_PATTERN",
48
+ "HOOK_EVENT_NAME",
49
+ "DENY_DECISION",
50
+ "CALLING_HOOK_NAME",
51
+ "CORRECTIVE_MESSAGE",
52
+ ]
53
+
54
+ BASH_TOOL_NAME = "Bash"
55
+ POWERSHELL_TOOL_NAME = "PowerShell"
56
+ ALL_SUPPORTED_TOOL_NAMES: frozenset[str] = frozenset(
57
+ {BASH_TOOL_NAME, POWERSHELL_TOOL_NAME}
58
+ )
59
+
60
+ ALL_FIND_PROGRAM_BASENAMES: frozenset[str] = frozenset({"find", "find.exe"})
61
+ ALL_POWERSHELL_LISTING_PROGRAM_BASENAMES: frozenset[str] = frozenset(
62
+ {"get-childitem", "gci", "dir"}
63
+ )
64
+ ALL_UNIX_LS_PROGRAM_BASENAMES: frozenset[str] = frozenset({"ls", "ls.exe"})
65
+ ALL_LISTING_PROGRAM_BASENAMES: frozenset[str] = (
66
+ ALL_POWERSHELL_LISTING_PROGRAM_BASENAMES | ALL_UNIX_LS_PROGRAM_BASENAMES
67
+ )
68
+
69
+ FLAG_AND_VALUE_TOKEN_STRIDE = 2
70
+ ALL_FIND_EXPRESSION_INTRODUCER_TOKENS: frozenset[str] = frozenset({"!", "(", ")"})
71
+
72
+ ALL_POWERSHELL_RECURSE_FLAGS: frozenset[str] = frozenset(
73
+ {"-recurse", "-r", "-rec", "/s"}
74
+ )
75
+ ALL_UNIX_LS_RECURSE_FLAGS: frozenset[str] = frozenset({"-R", "--recursive"})
76
+ UNIX_LS_RECURSE_SHORT_OPTION_LETTER = "R"
77
+ CLUSTERED_SHORT_OPTION_PATTERN = re.compile(r"^-[A-Za-z]+$")
78
+ ALL_POWERSHELL_RECURSE_FLAG_PREFIXES: tuple[str, ...] = (
79
+ "-recurse:",
80
+ "-r:",
81
+ )
82
+ ALL_POWERSHELL_PATH_FLAGS: frozenset[str] = frozenset(
83
+ {"-path", "-literalpath", "-lp"}
84
+ )
85
+ ALL_POWERSHELL_PATH_FLAG_PREFIXES: tuple[str, ...] = (
86
+ "-path:",
87
+ "-literalpath:",
88
+ "-lp:",
89
+ )
90
+ ALL_TRUTHY_RECURSE_COLON_VALUES: frozenset[str] = frozenset(
91
+ {"$true", "true", "1", "yes", "on"}
92
+ )
93
+
94
+ ALL_STRING_EXECUTING_SHELL_BASENAMES: frozenset[str] = frozenset(
95
+ {
96
+ "bash",
97
+ "bash.exe",
98
+ "sh",
99
+ "sh.exe",
100
+ "pwsh",
101
+ "pwsh.exe",
102
+ "powershell",
103
+ "powershell.exe",
104
+ }
105
+ )
106
+ ALL_STRING_EXEC_COMMAND_FLAGS: frozenset[str] = frozenset(
107
+ {"-c", "-lc", "-command", "-encodedcommand"}
108
+ )
109
+
110
+ _ALL_UNSCOPED_HOME_LITERALS: frozenset[str] = frozenset(
111
+ {
112
+ "~",
113
+ "~/",
114
+ "~\\",
115
+ "$HOME",
116
+ "$HOME/",
117
+ "$HOME\\",
118
+ "${HOME}",
119
+ "${HOME}/",
120
+ "${HOME}\\",
121
+ "%USERPROFILE%",
122
+ "%USERPROFILE%\\",
123
+ "%USERPROFILE%/",
124
+ "$env:USERPROFILE",
125
+ "$env:USERPROFILE\\",
126
+ "$env:USERPROFILE/",
127
+ "$env:HOME",
128
+ "$env:HOME\\",
129
+ "$env:HOME/",
130
+ }
131
+ )
132
+ ALL_UNSCOPED_HOME_LITERALS_CASEFOLD: frozenset[str] = frozenset(
133
+ each_literal.casefold() for each_literal in _ALL_UNSCOPED_HOME_LITERALS
134
+ )
135
+
136
+ GIT_BASH_DRIVE_ROOT_PATTERN = re.compile(r"^/[a-zA-Z]/?$")
137
+ WINDOWS_DRIVE_ROOT_PATTERN = re.compile(r"^[a-zA-Z]:[\\/]*$")
138
+ POSIX_ROOT_ALIAS_PATTERN = re.compile(r"^/(?:\.|/)*$")
139
+ UNC_SHARE_ROOT_PATTERN = re.compile(r"^//[^/]+/[^/]+/*$")
140
+
141
+ HOOK_EVENT_NAME = "PreToolUse"
142
+ DENY_DECISION = "deny"
143
+ CALLING_HOOK_NAME = "unscoped_search_blocker.py"
144
+
145
+ CORRECTIVE_MESSAGE = (
146
+ "Unscoped filesystem search blocked. Never start find/Get-ChildItem at `/`, "
147
+ "a drive root (`/c`, `C:\\`), bare home (`~`, `$HOME`), or a network share "
148
+ "root (`\\\\server\\share`). Scope the walk to a project or worktree path "
149
+ "(for example `find . -name '*.py'` or "
150
+ "`find packages/claude-dev-env -iname code_rules_gate.py`). On Windows prefer "
151
+ "es.exe with a path scope, or the Grep/Glob tools. Batch shell work; avoid "
152
+ "parallel full-tree searches that contend for the shell and lock the host."
153
+ )
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.5.0",
3
+ "version": "2.7.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
7
- "claude-dev-env": "bin/install.mjs"
7
+ "claude-dev-env": "bin/install.mjs",
8
+ "codex-compat": "bin/codex-compat.mjs"
8
9
  },
9
10
  "scripts": {
10
11
  "test": "node --test \"bin/*.test.mjs\" \"skills/**/*.test.mjs\""
@@ -19,6 +20,7 @@
19
20
  "hooks/",
20
21
  "system-prompts/",
21
22
  "scripts/",
23
+ "codex-capability-map.json",
22
24
  "_shared/",
23
25
  "audit-rubrics/",
24
26
  "CLAUDE.md",