claude-dev-env 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.agents/skills/everything-search/SKILL.md +18 -3
  2. package/.agents/skills/everything-search/scripts/everything_search.py +204 -0
  3. package/.agents/skills/everything-search/scripts/everything_search_command_constants/__init__.py +1 -0
  4. package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/__init__.py +1 -0
  5. package/.agents/skills/everything-search/scripts/everything_search_command_constants/config/constants.py +11 -0
  6. package/.agents/skills/everything-search/scripts/test_everything_search.py +172 -0
  7. package/.agents/skills/syncing-submodules/SKILL.md +101 -0
  8. package/.agents/skills/syncing-submodules/reference/run-tasks.md +9 -0
  9. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +383 -0
  10. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/__init__.py +1 -0
  11. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/__init__.py +1 -0
  12. package/.agents/skills/syncing-submodules/scripts/submodule_sync_constants/config/constants.py +66 -0
  13. package/.agents/skills/syncing-submodules/scripts/sync_parent_pointer.py +88 -0
  14. package/.agents/skills/syncing-submodules/scripts/test_submodule_sync.py +303 -0
  15. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +83 -0
  16. package/bin/ever-shipped-skills.mjs +1 -0
  17. package/bin/ever-shipped-skills.test.mjs +30 -0
  18. package/bin/git_hooks_installer.mjs +0 -1
  19. package/bin/git_hooks_installer.test.mjs +16 -1
  20. package/bin/install.mjs +5 -2
  21. package/bin/install.prune.test.mjs +95 -24
  22. package/bin/install.test.mjs +7 -0
  23. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +5 -8
  24. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +7 -13
  25. package/package.json +1 -1
  26. package/rules/filesystem-search.md +5 -5
  27. package/hooks/blocking/es_exe_path_rewriter.py +0 -159
  28. package/hooks/blocking/test_es_exe_path_rewriter.py +0 -447
  29. package/hooks/git-hooks/post_commit.py +0 -241
  30. package/hooks/git-hooks/test_post_commit.py +0 -298
  31. package/hooks/hooks_constants/path_rewriter_constants.py +0 -13
  32. package/hooks/hooks_constants/test_path_rewriter_constants.py +0 -57
@@ -35,7 +35,9 @@ BASH_TOOL_NAME = "Bash"
35
35
  POWERSHELL_TOOL_NAME = "PowerShell"
36
36
 
37
37
  ALL_BASH_ONLY_TOOL_NAMES: frozenset[str] = frozenset({BASH_TOOL_NAME})
38
- ALL_BASH_AND_POWERSHELL_TOOL_NAMES: frozenset[str] = frozenset({BASH_TOOL_NAME, POWERSHELL_TOOL_NAME})
38
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES: frozenset[str] = frozenset(
39
+ {BASH_TOOL_NAME, POWERSHELL_TOOL_NAME}
40
+ )
39
41
 
40
42
 
41
43
  @dataclass(frozen=True)
@@ -53,7 +55,6 @@ class BashHostedHookEntry:
53
55
 
54
56
 
55
57
  ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
56
- BashHostedHookEntry("blocking/es_exe_path_rewriter.py", ALL_BASH_ONLY_TOOL_NAMES),
57
58
  BashHostedHookEntry("blocking/destructive_command_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
58
59
  BashHostedHookEntry("blocking/gh_body_arg_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
59
60
  BashHostedHookEntry("blocking/shell_substitution_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
@@ -62,14 +63,10 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
62
63
  "blocking/cursor_cli_python_misfire_blocker.py",
63
64
  ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
64
65
  ),
65
- BashHostedHookEntry(
66
- "blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
67
- ),
66
+ BashHostedHookEntry("blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
68
67
  BashHostedHookEntry("blocking/nas_ssh_binary_enforcer.py", ALL_BASH_ONLY_TOOL_NAMES),
69
68
  BashHostedHookEntry("blocking/volatile_path_in_post_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
70
- BashHostedHookEntry(
71
- "blocking/pii_prevention_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
72
- ),
69
+ BashHostedHookEntry("blocking/pii_prevention_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
73
70
  BashHostedHookEntry("blocking/conventional_pr_title_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
74
71
  BashHostedHookEntry("blocking/block_main_commit.py", ALL_BASH_ONLY_TOOL_NAMES),
75
72
  BashHostedHookEntry("blocking/session_edit_stage_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
@@ -7,22 +7,18 @@ registration order the standalone chain used.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import sys
11
10
  from pathlib import Path
11
+ from runpy import run_path
12
12
 
13
- _HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
14
- if _HOOKS_DIR not in sys.path:
15
- sys.path.insert(0, _HOOKS_DIR)
16
-
17
- from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E402
18
- ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
19
- ALL_BASH_HOSTED_HOOK_ENTRIES,
20
- BASH_TOOL_NAME,
21
- POWERSHELL_TOOL_NAME,
13
+ ALL_CONSTANT_BINDINGS = run_path(
14
+ str(Path(__file__).with_name("bash_pre_tool_use_dispatcher_constants.py"))
22
15
  )
16
+ ALL_BASH_AND_POWERSHELL_TOOL_NAMES = ALL_CONSTANT_BINDINGS["ALL_BASH_AND_POWERSHELL_TOOL_NAMES"]
17
+ ALL_BASH_HOSTED_HOOK_ENTRIES = ALL_CONSTANT_BINDINGS["ALL_BASH_HOSTED_HOOK_ENTRIES"]
18
+ BASH_TOOL_NAME = ALL_CONSTANT_BINDINGS["BASH_TOOL_NAME"]
19
+ POWERSHELL_TOOL_NAME = ALL_CONSTANT_BINDINGS["POWERSHELL_TOOL_NAME"]
23
20
 
24
21
  _EXPECTED_BASH_ORDER = (
25
- "blocking/es_exe_path_rewriter.py",
26
22
  "blocking/destructive_command_blocker.py",
27
23
  "blocking/gh_body_arg_blocker.py",
28
24
  "blocking/shell_substitution_blocker.py",
@@ -80,8 +76,6 @@ def test_powershell_hooks_carry_the_shared_tool_set() -> None:
80
76
  assert each_entry.applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
81
77
 
82
78
 
83
-
84
-
85
79
  def test_every_roster_path_points_at_an_existing_hook() -> None:
86
80
  """Each roster path names a hook script present on disk, catching a typo."""
87
81
  hooks_root = Path(__file__).resolve().parent.parent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  # Filesystem Search
2
2
 
3
- **When this applies:** Any search for files by name, path, extension, size, or date — through `es.exe`, a shell `find`, a recursive `Get-ChildItem` / `gci` / `dir` / `ls -R`, or the harness Grep and Glob tools.
3
+ **When this applies:** Any search for files by name, path, extension, size, or date. That includes `es.exe`, a shell `find`, a recursive `Get-ChildItem` / `gci` / `dir` / `ls -R`, or the harness Grep and Glob tools.
4
4
 
5
5
  ## The scope invariant
6
6
 
@@ -14,7 +14,7 @@ Three tools are equally sanctioned; pick by what you know:
14
14
 
15
15
  | You know | Use |
16
16
  |---|---|
17
- | The exact path | `Read` — no search at all |
17
+ | The exact path | `Read`. Do not search. |
18
18
  | A name, extension, or date, on Windows | `es.exe` with a path scope |
19
19
  | A name or path pattern | The harness `Glob` tool |
20
20
  | Text inside files | The harness `Grep` tool |
@@ -39,13 +39,13 @@ When `es.exe` fails or returns nothing, fall back to `Glob` or `Grep` without pa
39
39
  | Git Bash drive root | `find /c -name '*.py'` |
40
40
  | Windows drive root | `find C:\ -name foo` / `Get-ChildItem C:\ -Recurse` |
41
41
  | Bare home | `find ~ -name README.md` / `find $HOME -type f` |
42
- | Network share root | `find //server/share -name x` — a path under the share (`//server/share/project/src`) is allowed |
42
+ | Network share root | `find //server/share -name x`. A path under the share (`//server/share/project/src`) is allowed |
43
43
 
44
44
  ## Shell batching
45
45
 
46
46
  Issue one shell search at a time when the walk is large. Parallel full-tree searches contend for the shell and can lock the host. Harness `Grep` and `Glob` calls carry no such cost and run in parallel freely.
47
47
 
48
- ## Enforcement
48
+ ## Search controls
49
49
 
50
50
  - `unscoped_search_blocker` (PreToolUse on Bash and PowerShell, hosted by `bash_pre_tool_use_dispatcher`) denies a walk from an unscoped root and returns the scoped alternative.
51
- - `es_exe_path_rewriter` (PreToolUse on Bash) substitutes `{project-name}` placeholders and bare registry keys in an `es.exe` command with their quoted absolute paths, read from `~/.claude/project-paths.json`. It allows and rewrites; it never blocks, and a machine with no registry file passes the command through unchanged. `scripts/setup_project_paths.py` writes the registry.
51
+ - For Everything searches that use a project name, run `python "${CLAUDE_SKILL_DIR}/scripts/everything_search.py" <project-name> <search arguments>`. An exact project name becomes its path from `~/.claude/project-paths.json`. The command then starts `es.exe` without a shell. `scripts/setup_project_paths.py` writes the registry.
@@ -1,159 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook: rewrite es.exe commands by substituting registry tokens with absolute paths.
3
-
4
- Reads tool_input.command from stdin JSON. When the command invokes the Everything
5
- command-line binary, substitutes {project-name} placeholder tokens and bare registry-key
6
- tokens with their quoted absolute paths from ~/.claude/project-paths.json before
7
- the Bash call runs. Never blocks or denies — on any error exits 0 with empty output.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import json
13
- import logging
14
- import re
15
- import sys
16
- from pathlib import Path, PurePosixPath, PureWindowsPath
17
-
18
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
19
- if _hooks_dir not in sys.path:
20
- sys.path.insert(0, _hooks_dir)
21
-
22
- from hooks_constants.dynamic_stderr_handler import DynamicStderrHandler # noqa: E402
23
- from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
24
- from hooks_constants.path_rewriter_constants import ( # noqa: E402
25
- BASH_TOOL_NAME,
26
- HOOK_EVENT_NAME,
27
- PERMISSION_ALLOW,
28
- PLACEHOLDER_TOKEN_PATTERN,
29
- )
30
- from hooks_constants.project_paths_reader import load_registry # noqa: E402
31
-
32
- _ES_EXE_TRIGGER_PATTERN = re.compile(
33
- r"(?i)(?<![\w.])(?:Everything[/\\])?es\.exe(?![\w.])",
34
- )
35
-
36
-
37
- _logger = logging.getLogger("es_exe_path_rewriter")
38
- if not _logger.handlers:
39
- _stderr_handler = DynamicStderrHandler()
40
- _stderr_handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
41
- _logger.addHandler(_stderr_handler)
42
- _logger.setLevel(logging.INFO)
43
- _logger.propagate = False
44
-
45
-
46
- def command_invokes_es_exe(command: str) -> bool:
47
- """Return True when the command string contains an es.exe invocation."""
48
- return bool(_ES_EXE_TRIGGER_PATTERN.search(command))
49
-
50
-
51
- def _token_is_absolute_path(token: str) -> bool:
52
- stripped = token.strip("\"'")
53
- try:
54
- return (
55
- PureWindowsPath(stripped).is_absolute()
56
- or PurePosixPath(stripped).is_absolute()
57
- )
58
- except (ValueError, TypeError):
59
- return False
60
-
61
-
62
- def _quote_path(absolute_path: str) -> str:
63
- return f'"{absolute_path}"'
64
-
65
-
66
- def _rewrite_placeholder_tokens(command_suffix: str, registry: dict[str, str]) -> str:
67
- def replace_placeholder(match: re.Match) -> str:
68
- inner_name = match.group(1)
69
- if inner_name not in registry:
70
- return match.group(0)
71
- return _quote_path(registry[inner_name])
72
-
73
- return PLACEHOLDER_TOKEN_PATTERN.sub(replace_placeholder, command_suffix)
74
-
75
-
76
- def _split_on_es_exe(command: str) -> tuple[str, str]:
77
- match = _ES_EXE_TRIGGER_PATTERN.search(command)
78
- if not match:
79
- return command, ""
80
- return command[: match.end()], command[match.end() :]
81
-
82
-
83
- def _strip_matching_outer_quotes(token: str) -> tuple[str, bool]:
84
- """Return (inner_text, was_quoted) after removing matched outer quotes."""
85
- if len(token) > 1 and token[0] in ('"', "'") and token[-1] == token[0]:
86
- return token[1:-1], True
87
- return token, False
88
-
89
-
90
- def _rewrite_bare_tokens(command_suffix: str, registry: dict[str, str]) -> str:
91
- all_raw_parts = re.split(r"(\s+)", command_suffix)
92
- all_rewritten_parts: list[str] = []
93
- for each_raw_part in all_raw_parts:
94
- if not each_raw_part or each_raw_part.isspace():
95
- all_rewritten_parts.append(each_raw_part)
96
- continue
97
- unquoted_text, _was_quoted = _strip_matching_outer_quotes(each_raw_part)
98
- if unquoted_text in registry and not _token_is_absolute_path(unquoted_text):
99
- all_rewritten_parts.append(_quote_path(registry[unquoted_text]))
100
- else:
101
- all_rewritten_parts.append(each_raw_part)
102
- return "".join(all_rewritten_parts)
103
-
104
-
105
- def rewrite_command(command: str, registry: dict[str, str]) -> str:
106
- """Apply registry substitutions to any es.exe argument tokens.
107
-
108
- Applies placeholder form {name} first, then bare-token form.
109
- Absolute-path arguments and unknown tokens are left untouched.
110
- Returns the original command when no substitution applies.
111
- """
112
- if not registry:
113
- return command
114
- prefix, suffix = _split_on_es_exe(command)
115
- rewritten_suffix = _rewrite_placeholder_tokens(suffix, registry)
116
- rewritten_suffix = _rewrite_bare_tokens(rewritten_suffix, registry)
117
- return prefix + rewritten_suffix
118
-
119
-
120
- def _build_allow_response(rewritten_command: str, original_tool_input: dict) -> dict:
121
- updated_input = {**original_tool_input, "command": rewritten_command}
122
- return {
123
- "hookSpecificOutput": {
124
- "hookEventName": HOOK_EVENT_NAME,
125
- "permissionDecision": PERMISSION_ALLOW,
126
- "updatedInput": updated_input,
127
- }
128
- }
129
-
130
-
131
- def main() -> None:
132
- try:
133
- hook_input = read_hook_input_dictionary_from_stdin()
134
- if hook_input is None:
135
- sys.exit(0)
136
- raw_tool_name = hook_input.get("tool_name", "")
137
- raw_tool_input = hook_input.get("tool_input", {})
138
- tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
139
- tool_input = raw_tool_input if isinstance(raw_tool_input, dict) else {}
140
- if tool_name != BASH_TOOL_NAME:
141
- sys.exit(0)
142
- raw_command = tool_input.get("command", "")
143
- command = raw_command if isinstance(raw_command, str) else ""
144
- if not command_invokes_es_exe(command):
145
- sys.exit(0)
146
- known_registry = load_registry()
147
- if not known_registry:
148
- sys.exit(0)
149
- rewritten_command = rewrite_command(command, known_registry)
150
- if rewritten_command == command:
151
- sys.exit(0)
152
- print(json.dumps(_build_allow_response(rewritten_command, tool_input)))
153
- except Exception as e:
154
- _logger.error("%s", e)
155
- sys.exit(0)
156
-
157
-
158
- if __name__ == "__main__":
159
- main()