claude-dev-env 8.32.2 → 8.33.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.
@@ -0,0 +1,202 @@
1
+ """Decide whether a shell command writes, sends, or changes state.
2
+
3
+ Every command string a wrapper runs is scanned, and each segment is read as
4
+ its program plus arguments after the wrappers in front of it::
5
+
6
+ sudo git push writes
7
+ bash -c "gh pr merge 12" writes
8
+ pwsh -Command git push; echo done writes
9
+ rg 'gh pr merge' docs reads
10
+ grep "a|cp b" notes.md reads
11
+
12
+ A write counts only when the POSIX tokenization and the raw tokenization of a
13
+ line agree on it, so a quoted literal never reads as a command.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import itertools
19
+
20
+ from hooks_constants.pytest_invocation import unquoted_token
21
+ from hooks_constants.shell_command_pipeline import (
22
+ all_operator_aware_tokenizations,
23
+ join_line_continuations,
24
+ scannable_command_lines,
25
+ segments_with_following_operator,
26
+ )
27
+ from hooks_constants.shell_command_segments import git_subcommand_tokens, token_basename
28
+ from hooks_constants.shell_command_wrappers import (
29
+ all_wrapped_command_texts,
30
+ segment_program_and_arguments,
31
+ )
32
+ from hooks_constants.verify_before_acting_constants import (
33
+ ALL_FILE_WRITING_PROGRAM_NAMES,
34
+ ALL_GH_API_METHOD_OPTIONS,
35
+ ALL_HTTP_WRITE_METHODS,
36
+ ALL_MUTATING_GH_SUBCOMMANDS,
37
+ ALL_MUTATING_GIT_SUBCOMMAND_PREFIXES,
38
+ ALL_NON_FILE_REDIRECTION_TARGETS,
39
+ ALL_POWERSHELL_WRITE_CMDLET_NAMES,
40
+ ALL_PULL_REQUEST_SCRIPT_WRITE_ACTIONS,
41
+ ALL_WRITE_REDIRECTION_OPERATORS,
42
+ DESCRIPTOR_CLOSE_TARGET,
43
+ DESCRIPTOR_DUPLICATION_OPERATOR,
44
+ GH_API_ATTACHED_METHOD_PATTERN,
45
+ GH_API_FIELD_OPTION_PATTERN,
46
+ GH_API_SUBCOMMAND,
47
+ GH_PROGRAM_NAME,
48
+ GH_SUBCOMMAND_DEPTH,
49
+ GIT_PROGRAM_NAME,
50
+ POWERSHELL_SCRIPT_BLOCK_OPENER,
51
+ POWERSHELL_WORD_BRACKETS,
52
+ PULL_REQUEST_SCRIPT_NAME,
53
+ REDIRECTION_TARGET_QUOTES,
54
+ SED_IN_PLACE_OPTION_PATTERN,
55
+ SED_PROGRAM_NAME,
56
+ )
57
+
58
+ __all__ = ["is_mutating_shell_command"]
59
+
60
+
61
+ def _is_mutating_git_arguments(all_arguments: list[str]) -> bool:
62
+ all_subcommand_tokens = git_subcommand_tokens(all_arguments)
63
+ return any(
64
+ tuple(all_subcommand_tokens[: len(each_prefix)]) == each_prefix
65
+ for each_prefix in ALL_MUTATING_GIT_SUBCOMMAND_PREFIXES
66
+ )
67
+
68
+
69
+ def _gh_api_method(all_api_arguments: list[str]) -> str | None:
70
+ for each_option, each_method_argument in itertools.pairwise(all_api_arguments):
71
+ if each_option in ALL_GH_API_METHOD_OPTIONS:
72
+ return each_method_argument.upper()
73
+ for each_argument in all_api_arguments:
74
+ attached_match = GH_API_ATTACHED_METHOD_PATTERN.fullmatch(each_argument)
75
+ if attached_match is not None:
76
+ return attached_match.group("method").upper()
77
+ return None
78
+
79
+
80
+ def _is_mutating_gh_api(all_api_arguments: list[str]) -> bool:
81
+ """Return True when ``gh api`` sends a write method, or fields or ``--input`` with no method named."""
82
+ method = _gh_api_method(all_api_arguments)
83
+ if method is not None:
84
+ return method in ALL_HTTP_WRITE_METHODS
85
+ return any(
86
+ GH_API_FIELD_OPTION_PATTERN.fullmatch(each_argument) for each_argument in all_api_arguments
87
+ )
88
+
89
+
90
+ def _is_mutating_gh_arguments(all_arguments: list[str]) -> bool:
91
+ if tuple(all_arguments[:GH_SUBCOMMAND_DEPTH]) in ALL_MUTATING_GH_SUBCOMMANDS:
92
+ return True
93
+ return all_arguments[:1] == [GH_API_SUBCOMMAND] and _is_mutating_gh_api(all_arguments[1:])
94
+
95
+
96
+ def _program_mutates(program_name: str, all_arguments: list[str]) -> bool:
97
+ if program_name in ALL_FILE_WRITING_PROGRAM_NAMES:
98
+ return True
99
+ if program_name == SED_PROGRAM_NAME:
100
+ return any(
101
+ SED_IN_PLACE_OPTION_PATTERN.fullmatch(each_argument) for each_argument in all_arguments
102
+ )
103
+ if program_name == GIT_PROGRAM_NAME:
104
+ return _is_mutating_git_arguments(all_arguments)
105
+ return program_name == GH_PROGRAM_NAME and _is_mutating_gh_arguments(all_arguments)
106
+
107
+
108
+ def _runs_a_pull_request_script_write(all_segment_tokens: list[str]) -> bool:
109
+ for each_index, each_token in enumerate(all_segment_tokens):
110
+ if token_basename(unquoted_token(each_token)) == PULL_REQUEST_SCRIPT_NAME:
111
+ return any(
112
+ unquoted_token(each_later_token) in ALL_PULL_REQUEST_SCRIPT_WRITE_ACTIONS
113
+ for each_later_token in all_segment_tokens[each_index + 1 :]
114
+ )
115
+ return False
116
+
117
+
118
+ def _powershell_command_words(program_name: str, all_segment_tokens: list[str]) -> list[str]:
119
+ """Return the words a segment runs as commands: its program and each script block's first word.
120
+
121
+ ``ForEach-Object { Remove-Item $_ }`` runs ``Remove-Item`` as a command, while
122
+ ``Get-Command Remove-Item`` passes it as an argument.
123
+ """
124
+ all_command_words = [program_name]
125
+ for each_previous_token, each_token in itertools.pairwise(all_segment_tokens):
126
+ if each_previous_token.endswith(POWERSHELL_SCRIPT_BLOCK_OPENER):
127
+ all_command_words.append(each_token)
128
+ all_command_words.extend(
129
+ each_token.rpartition(POWERSHELL_SCRIPT_BLOCK_OPENER)[2]
130
+ for each_token in all_segment_tokens
131
+ if POWERSHELL_SCRIPT_BLOCK_OPENER in each_token
132
+ )
133
+ return all_command_words
134
+
135
+
136
+ def _runs_a_powershell_write_cmdlet(program_name: str, all_segment_tokens: list[str]) -> bool:
137
+ return any(
138
+ each_word.strip(POWERSHELL_WORD_BRACKETS).lower() in ALL_POWERSHELL_WRITE_CMDLET_NAMES
139
+ for each_word in _powershell_command_words(program_name, all_segment_tokens)
140
+ )
141
+
142
+
143
+ def _redirect_target_is_a_file(operator: str, target: str) -> bool:
144
+ """Return True when a redirect writes a file.
145
+
146
+ A digit or ``-`` after ``>&`` duplicates or closes a descriptor, as in
147
+ ``2>&1`` and ``>&-``. After any other operator it names a file, so
148
+ ``echo hi > 1`` writes one.
149
+ """
150
+ target_text = target.strip(REDIRECTION_TARGET_QUOTES).lower()
151
+ if target_text in ALL_NON_FILE_REDIRECTION_TARGETS:
152
+ return False
153
+ return not (
154
+ operator == DESCRIPTOR_DUPLICATION_OPERATOR
155
+ and (target_text.isdigit() or target_text == DESCRIPTOR_CLOSE_TARGET)
156
+ )
157
+
158
+
159
+ def _segment_redirects_into_a_file(all_segment_tokens: list[str]) -> bool:
160
+ return any(
161
+ each_operator in ALL_WRITE_REDIRECTION_OPERATORS
162
+ and _redirect_target_is_a_file(each_operator, each_target)
163
+ for each_operator, each_target in itertools.pairwise(all_segment_tokens)
164
+ )
165
+
166
+
167
+ def _segment_is_mutating(all_segment_tokens: list[str]) -> bool:
168
+ program_name, all_arguments = segment_program_and_arguments(all_segment_tokens)
169
+ return (
170
+ _program_mutates(program_name, all_arguments)
171
+ or _runs_a_pull_request_script_write(all_segment_tokens)
172
+ or _runs_a_powershell_write_cmdlet(program_name, all_segment_tokens)
173
+ or _segment_redirects_into_a_file(all_segment_tokens)
174
+ )
175
+
176
+
177
+ def _line_is_mutating(command_line: str) -> bool:
178
+ """Return True when every quote-aware tokenization of the line has a mutating segment.
179
+
180
+ The POSIX tokenization strips the quotes from ``rg 'Remove-Item' docs`` and
181
+ the raw one splits ``--format='%h > %s'`` mid-quote, so a write counts only
182
+ when both spellings agree on it.
183
+ """
184
+ all_tokenizations = all_operator_aware_tokenizations(command_line)
185
+ return bool(all_tokenizations) and all(
186
+ any(
187
+ _segment_is_mutating(each_segment)
188
+ for each_segment, _following_operator in segments_with_following_operator(
189
+ each_tokenization
190
+ )
191
+ )
192
+ for each_tokenization in all_tokenizations
193
+ )
194
+
195
+
196
+ def is_mutating_shell_command(command: str) -> bool:
197
+ """Return True when any line of the command, or of a command string it wraps, writes."""
198
+ return any(
199
+ _line_is_mutating(each_line)
200
+ for each_command_text in all_wrapped_command_texts(command)
201
+ for each_line in scannable_command_lines(join_line_continuations(each_command_text))
202
+ )
@@ -14,21 +14,28 @@ bodies dropped, parenthesis groups joined — lives in
14
14
  from __future__ import annotations
15
15
 
16
16
  import re
17
+ import shlex
17
18
 
18
19
  __all__ = [
19
20
  "ALL_LAUNCHER_WRAPPER_COMMANDS",
20
21
  "ALL_SHELL_CONTROL_OPERATOR_TOKENS",
22
+ "ALL_GIT_GLOBAL_OPTIONS_WITH_VALUE",
21
23
  "CONTROL_OPERATOR_SPLIT_PATTERN",
22
24
  "LEADING_ASSIGNMENT_PATTERN",
23
25
  "LAUNCHER_DURATION_PATTERN",
24
26
  "token_basename",
25
27
  "split_into_segments",
26
28
  "effective_leading_program",
29
+ "command_tokens",
30
+ "git_subcommand_tokens",
27
31
  ]
28
32
 
29
33
  ALL_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
30
34
  {"timeout", "nohup", "nice", "stdbuf", "setsid", "env", "time"}
31
35
  )
36
+ ALL_GIT_GLOBAL_OPTIONS_WITH_VALUE: frozenset[str] = frozenset(
37
+ {"-C", "-c", "--git-dir", "--work-tree", "--namespace"}
38
+ )
32
39
  ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset(
33
40
  {"&&", "||", ";", "|", "&", "|&"}
34
41
  )
@@ -85,3 +92,35 @@ def effective_leading_program(all_segment_tokens: list[str]) -> str | None:
85
92
  continue
86
93
  return each_token
87
94
  return None
95
+
96
+
97
+ def command_tokens(command: str) -> list[str]:
98
+ """Return the POSIX shlex tokens of a command, or its whitespace split when quoting is unbalanced."""
99
+ try:
100
+ return shlex.split(command, posix=True)
101
+ except ValueError:
102
+ return command.split()
103
+
104
+
105
+ def git_subcommand_tokens(all_arguments: list[str]) -> list[str]:
106
+ """Return git's subcommand and the tokens after it, past git's global options.
107
+
108
+ ``git -C <path> push`` and ``git --git-dir <path> push`` carry an option
109
+ value before the subcommand; ``--git-dir=<path>`` and ``--no-pager`` carry
110
+ none. The first token left after those starts the result, so
111
+ ``git stash push`` yields ``["stash", "push"]`` and ``git log --grep push``
112
+ yields ``["log", "--grep", "push"]``. A command with no subcommand yields
113
+ an empty list.
114
+ """
115
+ should_skip_next_token = False
116
+ for each_index, each_argument in enumerate(all_arguments):
117
+ if should_skip_next_token:
118
+ should_skip_next_token = False
119
+ continue
120
+ if each_argument in ALL_GIT_GLOBAL_OPTIONS_WITH_VALUE:
121
+ should_skip_next_token = True
122
+ continue
123
+ if each_argument.startswith("-"):
124
+ continue
125
+ return all_arguments[each_index:]
126
+ return []
@@ -0,0 +1,68 @@
1
+ """Step over the wrappers in front of a shell command to reach the command it runs.
2
+
3
+ A wrapper either runs its own operands, as ``sudo``, ``xargs``, ``env``, and
4
+ ``timeout`` do, or runs a command string, as ``bash -c``, ``sh -lc``, and
5
+ ``pwsh -Command`` do::
6
+
7
+ sudo git push git, ["push"]
8
+ env GIT_DIR=x git push git, ["push"]
9
+ echo origin | xargs git push git, ["push"]
10
+ bash -lc "git commit -F m.txt" "git commit -F m.txt" unwraps in turn
11
+
12
+ ``pytest_invocation`` owns the wrapper and string-exec parsing, and this module
13
+ composes it into the two answers a hook asks about any program.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from hooks_constants.piped_pytest_blocker_constants import WINDOWS_EXECUTABLE_SUFFIX
19
+ from hooks_constants.pytest_invocation import (
20
+ all_tokens_after_wrappers,
21
+ string_exec_inner_command,
22
+ unquoted_token,
23
+ )
24
+ from hooks_constants.shell_command_pipeline import pipeline_segments_for_command
25
+ from hooks_constants.shell_command_segments import (
26
+ effective_leading_program,
27
+ token_basename,
28
+ )
29
+
30
+ __all__ = [
31
+ "segment_program_and_arguments",
32
+ "all_wrapped_command_texts",
33
+ ]
34
+
35
+
36
+ def segment_program_and_arguments(all_segment_tokens: list[str]) -> tuple[str, list[str]]:
37
+ """Return a segment's program basename past its wrappers and the tokens after it.
38
+
39
+ The basename drops a ``.exe`` suffix, so ``git.exe push`` reads as ``git``.
40
+ Returns an empty pair when the segment names no program.
41
+ """
42
+ all_unwrapped_tokens = all_tokens_after_wrappers(all_segment_tokens)
43
+ program_token = effective_leading_program(all_unwrapped_tokens)
44
+ if program_token is None:
45
+ return "", []
46
+ program_index = all_unwrapped_tokens.index(program_token)
47
+ program_name = token_basename(unquoted_token(program_token)).removesuffix(
48
+ WINDOWS_EXECUTABLE_SUFFIX
49
+ )
50
+ return program_name, all_unwrapped_tokens[program_index + 1 :]
51
+
52
+
53
+ def all_wrapped_command_texts(command: str) -> list[str]:
54
+ """Return the command plus the command string every shell wrapper inside it runs.
55
+
56
+ The segments come from the quote-aware pipeline parser, so the quoted
57
+ script in ``powershell -Command "git add -A; git push"`` stays whole until
58
+ its own turn. Nested wrappers unwrap in turn.
59
+ """
60
+ all_inner_commands = [
61
+ string_exec_inner_command(each_segment)
62
+ for each_segment, _following_operator in pipeline_segments_for_command(command)
63
+ ]
64
+ all_command_texts = [command]
65
+ for each_inner_command in all_inner_commands:
66
+ if each_inner_command is not None:
67
+ all_command_texts.extend(all_wrapped_command_texts(each_inner_command))
68
+ return all_command_texts
@@ -12,17 +12,6 @@ def test_gh_probe_asks_for_every_field_the_checklist_reads() -> None:
12
12
  assert each_field in all_requested_fields
13
13
 
14
14
 
15
- def test_git_global_options_that_take_a_value_are_named() -> None:
16
- assert constants.ALL_GIT_OPTIONS_WITH_VALUE == frozenset({"-C", "-c"})
17
-
18
-
19
- def test_powershell_wrappers_and_their_command_flags_are_named() -> None:
20
- assert "pwsh" in constants.ALL_POWERSHELL_PROGRAM_NAMES
21
- assert "powershell" in constants.ALL_POWERSHELL_PROGRAM_NAMES
22
- assert "-command" in constants.ALL_POWERSHELL_COMMAND_FLAGS
23
- assert "-c" in constants.ALL_POWERSHELL_COMMAND_FLAGS
24
-
25
-
26
15
  def test_checklist_lines_join_on_a_single_newline() -> None:
27
16
  assert constants.REMINDER_LINE_SEPARATOR == "\n"
28
17
 
@@ -36,6 +36,12 @@ def _first_segment_tokens(command: str) -> list[str]:
36
36
  ["sudo", "pytest", "tests"],
37
37
  ["sudo", "-u", "ci", "pytest", "tests"],
38
38
  ["sudo", "-nu", "ci", "pytest", "tests"],
39
+ ["xargs", "pytest"],
40
+ ["xargs", "-0", "pytest"],
41
+ ["xargs", "-r", "pytest"],
42
+ ["xargs", "-n", "1", "pytest"],
43
+ ["env", "-u", "HOME", "pytest"],
44
+ ["timeout", "--signal", "KILL", "60", "pytest"],
39
45
  ["uv", "run", "pytest", "tests"],
40
46
  ["uv", "run", "--frozen", "pytest", "tests"],
41
47
  ["uv", "tool", "run", "pytest", "tests"],
@@ -0,0 +1,37 @@
1
+ """Pin which shell commands the mutation check reads as writes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from hooks_constants.shell_command_mutation import is_mutating_shell_command
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ "command",
12
+ [
13
+ "sudo git push",
14
+ 'bash -c "git commit -F m.txt"',
15
+ "pwsh -Command git push origin HEAD; echo done",
16
+ "gh api repos/o/r/issues -f title=x",
17
+ "Get-ChildItem | ForEach-Object { Remove-Item $_ }",
18
+ "echo hi > notes.txt",
19
+ ],
20
+ )
21
+ def test_should_read_a_write_as_mutating(command: str) -> None:
22
+ assert is_mutating_shell_command(command) is True
23
+
24
+
25
+ @pytest.mark.parametrize(
26
+ "command",
27
+ [
28
+ "rg 'gh pr merge' docs",
29
+ "rg 'Remove-Item' docs",
30
+ 'grep "a|cp b" notes.md',
31
+ "gh api -X GET search/issues -f q=x",
32
+ 'bash -c "git status"',
33
+ "git log --format='%h > %s'",
34
+ ],
35
+ )
36
+ def test_should_read_a_check_as_not_mutating(command: str) -> None:
37
+ assert is_mutating_shell_command(command) is False
@@ -0,0 +1,29 @@
1
+ """Pin git global-option parsing in the shared segment helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from hooks_constants import shell_command_segments
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ ("all_arguments", "all_expected_tokens"),
12
+ [
13
+ (["push", "origin", "HEAD"], ["push", "origin", "HEAD"]),
14
+ (["-C", "/repo", "push"], ["push"]),
15
+ (["-c", "user.name=x", "commit", "-F", "m.txt"], ["commit", "-F", "m.txt"]),
16
+ (["--git-dir=/repo/.git", "push"], ["push"]),
17
+ (["--git-dir", "/repo/.git", "push"], ["push"]),
18
+ (["--work-tree", "/repo", "commit"], ["commit"]),
19
+ (["--namespace", "n", "push"], ["push"]),
20
+ (["--no-pager", "log"], ["log"]),
21
+ (["stash", "push", "-u"], ["stash", "push", "-u"]),
22
+ (["log", "--grep", "push"], ["log", "--grep", "push"]),
23
+ (["--version"], []),
24
+ ],
25
+ )
26
+ def test_should_return_the_subcommand_tokens_past_global_options(
27
+ all_arguments: list[str], all_expected_tokens: list[str]
28
+ ) -> None:
29
+ assert shell_command_segments.git_subcommand_tokens(all_arguments) == all_expected_tokens
@@ -0,0 +1,61 @@
1
+ """Pin wrapper stepping and command-string unwrapping in the shared wrapper helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from hooks_constants import shell_command_wrappers
8
+
9
+
10
+ @pytest.mark.parametrize(
11
+ ("command", "inner_command"),
12
+ [
13
+ ('pwsh -NoProfile -Command "git add -A; git push"', "git add -A; git push"),
14
+ ("pwsh -Command git push origin HEAD; echo done", "git push origin HEAD"),
15
+ ('bash -c "git push"', "git push"),
16
+ ("sh -c 'git commit -F m.txt'", "git commit -F m.txt"),
17
+ ("bash -lc 'git push'", "git push"),
18
+ ("sudo bash -c 'git push'", "git push"),
19
+ ("bash -c \"pwsh -Command 'git push'\"", "git push"),
20
+ ],
21
+ )
22
+ def test_should_return_the_command_string_a_shell_wrapper_runs(
23
+ command: str, inner_command: str
24
+ ) -> None:
25
+ all_command_texts = shell_command_wrappers.all_wrapped_command_texts(command)
26
+ assert all_command_texts[0] == command
27
+ assert inner_command in all_command_texts
28
+
29
+
30
+ def test_should_return_only_the_command_when_no_wrapper_runs() -> None:
31
+ assert shell_command_wrappers.all_wrapped_command_texts("git push") == ["git push"]
32
+
33
+
34
+ @pytest.mark.parametrize(
35
+ ("all_segment_tokens", "expected_program_and_arguments"),
36
+ [
37
+ (["git", "push"], ("git", ["push"])),
38
+ (["sudo", "git", "push"], ("git", ["push"])),
39
+ (["sudo", "-u", "root", "git", "push"], ("git", ["push"])),
40
+ (["env", "VAR=x", "git", "push"], ("git", ["push"])),
41
+ (["xargs", "git", "push"], ("git", ["push"])),
42
+ (["xargs", "-r", "rm"], ("rm", [])),
43
+ (["xargs", "-I", "{}", "rm", "{}"], ("rm", ["{}"])),
44
+ (["xargs", "-n", "1", "git", "push"], ("git", ["push"])),
45
+ (["env", "-u", "HOME", "git", "push"], ("git", ["push"])),
46
+ (["env", "-i", "VAR=x", "git", "push"], ("git", ["push"])),
47
+ (["timeout", "--signal", "KILL", "5", "git", "push"], ("git", ["push"])),
48
+ (["timeout", "--signal=KILL", "5", "git", "push"], ("git", ["push"])),
49
+ (["sudo", "-nu", "ci", "env", "-u", "HOME", "git", "push"], ("git", ["push"])),
50
+ (["time", "sudo", "git", "push"], ("git", ["push"])),
51
+ (["'/usr/bin/git'", "push"], ("git", ["push"])),
52
+ (["VAR=x"], ("", [])),
53
+ ],
54
+ )
55
+ def test_should_return_the_program_past_its_wrappers(
56
+ all_segment_tokens: list[str], expected_program_and_arguments: tuple[str, list[str]]
57
+ ) -> None:
58
+ assert (
59
+ shell_command_wrappers.segment_program_and_arguments(all_segment_tokens)
60
+ == expected_program_and_arguments
61
+ )
@@ -0,0 +1,170 @@
1
+ """Constants for the verify-before-acting PostToolUse hook."""
2
+
3
+ import re
4
+
5
+ from hooks_constants.tool_names import APPLY_PATCH_TOOL_NAME
6
+
7
+ ALLOW_EXIT_CODE = 0
8
+ TOOL_NAME_KEY = "tool_name"
9
+ TOOL_INPUT_KEY = "tool_input"
10
+ TOOL_USE_ID_KEY = "tool_use_id"
11
+ TRANSCRIPT_PATH_KEY = "transcript_path"
12
+ COMMAND_KEY = "command"
13
+ MESSAGE_KEY = "message"
14
+ MESSAGE_ID_KEY = "id"
15
+ CONTENT_KEY = "content"
16
+ BLOCK_TYPE_KEY = "type"
17
+ BLOCK_ID_KEY = "id"
18
+ THINKING_BLOCK_TYPE = "thinking"
19
+ THINKING_TEXT_KEY = "thinking"
20
+ TOOL_USE_BLOCK_TYPE = "tool_use"
21
+ TRANSCRIPT_ENCODING = "utf-8"
22
+ TRANSCRIPT_DECODE_ERRORS = "replace"
23
+ THINKING_JOINER = "\n"
24
+ TRANSCRIPT_POLL_INTERVAL_SECONDS = 0.05
25
+ TRANSCRIPT_POLL_LIMIT_SECONDS = 5.0
26
+
27
+ ALL_ALWAYS_MUTATING_TOOL_NAMES = frozenset(
28
+ {"Write", "Edit", "MultiEdit", "NotebookEdit", "Agent", "Task", APPLY_PATCH_TOOL_NAME}
29
+ )
30
+ ALL_SHELL_TOOL_NAMES = frozenset({"Bash", "PowerShell"})
31
+ MCP_TOOL_PREFIX = "mcp__"
32
+ MCP_SEGMENT_SEPARATOR = "__"
33
+ MCP_ACTION_WORD_SPLIT_PATTERN = re.compile(r"_+|(?<=[a-z0-9])(?=[A-Z])")
34
+ ALL_MCP_MUTATING_VERBS = frozenset(
35
+ {
36
+ "create",
37
+ "update",
38
+ "delete",
39
+ "send",
40
+ "post",
41
+ "reply",
42
+ "write",
43
+ "merge",
44
+ "set",
45
+ "add",
46
+ "remove",
47
+ "edit",
48
+ "trash",
49
+ "archive",
50
+ }
51
+ )
52
+ ALL_MCP_READ_VERBS = frozenset({"get", "list", "search", "read", "view", "fetch", "find", "query"})
53
+ ALL_MCP_MUTATING_ACTION_NAMES = frozenset({"request_copilot_review"})
54
+
55
+ GIT_PROGRAM_NAME = "git"
56
+ ALL_MUTATING_GIT_SUBCOMMAND_PREFIXES = frozenset(
57
+ {
58
+ ("commit",),
59
+ ("push",),
60
+ ("merge",),
61
+ ("rebase",),
62
+ ("reset",),
63
+ ("checkout", "-b"),
64
+ ("switch", "-c"),
65
+ ("worktree", "add"),
66
+ }
67
+ )
68
+ ALL_WRITE_REDIRECTION_OPERATORS = frozenset({">", ">>", "&>", "&>>", ">&", ">|"})
69
+ ALL_NON_FILE_REDIRECTION_TARGETS = frozenset({"/dev/null", "$null", "nul"})
70
+ DESCRIPTOR_DUPLICATION_OPERATOR = ">&"
71
+ DESCRIPTOR_CLOSE_TARGET = "-"
72
+ REDIRECTION_TARGET_QUOTES = "'\""
73
+
74
+ ALL_FILE_WRITING_PROGRAM_NAMES = frozenset(
75
+ {"rm", "rmdir", "unlink", "touch", "cp", "mv", "mkdir", "tee", "ln"}
76
+ )
77
+ SED_PROGRAM_NAME = "sed"
78
+ SED_IN_PLACE_OPTION_PATTERN = re.compile(r"-[a-zA-Z]*i.*|--in-place(?:=.*)?")
79
+ GH_PROGRAM_NAME = "gh"
80
+ GH_SUBCOMMAND_DEPTH = 2
81
+ ALL_MUTATING_GH_SUBCOMMANDS = frozenset(
82
+ {
83
+ ("pr", "create"),
84
+ ("pr", "edit"),
85
+ ("pr", "merge"),
86
+ ("pr", "comment"),
87
+ ("pr", "ready"),
88
+ ("pr", "close"),
89
+ ("pr", "review"),
90
+ ("issue", "create"),
91
+ ("issue", "edit"),
92
+ ("issue", "comment"),
93
+ ("issue", "close"),
94
+ ("run", "rerun"),
95
+ ("workflow", "run"),
96
+ }
97
+ )
98
+ GH_API_SUBCOMMAND = "api"
99
+ ALL_GH_API_METHOD_OPTIONS = frozenset({"-X", "--method"})
100
+ GH_API_ATTACHED_METHOD_PATTERN = re.compile(r"(?:-X|--method=)(?P<method>.+)")
101
+ GH_API_FIELD_OPTION_PATTERN = re.compile(r"-[fF].*|--(?:field|raw-field|input)(?:=.*)?")
102
+ ALL_HTTP_WRITE_METHODS = frozenset({"POST", "PATCH", "PUT", "DELETE"})
103
+ PULL_REQUEST_SCRIPT_NAME = "pull_request.py"
104
+ ALL_PULL_REQUEST_SCRIPT_WRITE_ACTIONS = frozenset({"create", "edit", "comment", "review"})
105
+ ALL_POWERSHELL_WRITE_CMDLET_NAMES = frozenset(
106
+ {
107
+ "remove-item",
108
+ "set-content",
109
+ "add-content",
110
+ "out-file",
111
+ "copy-item",
112
+ "move-item",
113
+ "new-item",
114
+ "rename-item",
115
+ }
116
+ )
117
+ POWERSHELL_WORD_BRACKETS = "{}();"
118
+ POWERSHELL_SCRIPT_BLOCK_OPENER = "{"
119
+
120
+ ALL_HEDGE_PHRASES = (
121
+ "probably",
122
+ "likely",
123
+ "maybe",
124
+ "perhaps",
125
+ "presumably",
126
+ "might be",
127
+ "may be",
128
+ "seems",
129
+ "appears to",
130
+ "i suspect",
131
+ "i guess",
132
+ "i believe",
133
+ "my guess",
134
+ "my theory",
135
+ "not sure",
136
+ "unsure",
137
+ )
138
+ HEDGE_PATTERN = re.compile(
139
+ r"\b(?:"
140
+ + "|".join(r"\s+".join(each_phrase.split()) for each_phrase in ALL_HEDGE_PHRASES)
141
+ + r")\b",
142
+ re.IGNORECASE,
143
+ )
144
+ SENTENCE_SPLIT_PATTERN = re.compile(r"(?<=[.!?])\s+|\n+")
145
+ WHITESPACE_RUN_PATTERN = re.compile(r"\s+")
146
+ WORD_SEPARATOR = " "
147
+ MAXIMUM_QUOTE_LENGTH = 160
148
+ QUOTE_LEAD_LENGTH = MAXIMUM_QUOTE_LENGTH // 2
149
+ TRIM_MARKER = "..."
150
+
151
+ DECISION_KEY = "decision"
152
+ BLOCK_DECISION = "block"
153
+ REASON_KEY = "reason"
154
+ BLOCK_REASON_TEMPLATE = (
155
+ 'The reasoning behind this {tool_name} call hedges: "{hedge_sentence}" '
156
+ "Check that claim now with a read-only tool, then continue. "
157
+ "If the check contradicts it, undo this change first."
158
+ )
159
+
160
+ DECISION_LOG_RELATIVE_PATH = ".claude/logs/verify-before-acting.jsonl"
161
+ LOG_APPEND_MODE = "a"
162
+ LOG_LINE_END = "\n"
163
+ LOG_TIMESTAMP_KEY = "timestamp"
164
+ LOG_TOOL_NAME_KEY = "tool_name"
165
+ LOG_TOOL_USE_ID_KEY = "tool_use_id"
166
+ LOG_OUTCOME_KEY = "outcome"
167
+ LOG_HEDGE_SENTENCE_KEY = "hedge_sentence"
168
+ OUTCOME_BLOCKED = "blocked"
169
+ OUTCOME_ALLOWED_CLEAN = "allowed_clean"
170
+ OUTCOME_REASONING_UNSEEN = "reasoning_unseen"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.32.2",
3
+ "version": "8.33.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {