claude-dev-env 2.5.0 → 2.7.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/CLAUDE.md +20 -57
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/agents/CLAUDE.md +1 -1
- package/agents/code-verifier.md +36 -7
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/codex-capability-map.json +13 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/code-review-enforcement.md +31 -6
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +236 -27
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +0 -12
- package/hooks/hooks_constants/CLAUDE.md +5 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/package.json +4 -2
- package/rules/CLAUDE.md +17 -23
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +10 -2
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_invoke_code_review.py +143 -0
- package/scripts/test_invoke_code_review_chain.py +1 -1
- package/scripts/test_invoke_code_review_contract.py +1 -1
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/autoconverge/workflow/converge.mjs +1 -1
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/condensing-instructions/SKILL.md +42 -51
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/orchestrator/SKILL.md +23 -9
- package/skills/orchestrator-refresh/SKILL.md +20 -1
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/session-log/SKILL.md +1 -1
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/state-what-is.md +0 -25
- 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,
|
|
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
|
|
5
|
-
|
|
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
|
+
)
|
|
@@ -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
|
|
73
|
-
"""PowerShell runs the PII
|
|
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
|
|
@@ -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.
|
|
3
|
+
"version": "2.7.0",
|
|
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",
|