claude-dev-env 1.40.0 → 1.42.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 +9 -1
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +231 -3
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +56 -2
- package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +2 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +173 -6
- package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +135 -14
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +33 -0
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +4 -2
- package/hooks/_gh_pr_author_swap_utils.py +1211 -0
- package/hooks/blocking/gh_body_arg_blocker.py +9 -6
- package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
- package/hooks/blocking/gh_pr_author_restore.py +100 -0
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
- package/hooks/blocking/pr_description_enforcer.py +1 -3
- package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
- package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
- package/hooks/config/gh_pr_author_swap_constants.py +76 -0
- package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
- package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
- package/hooks/config/pr_description_enforcer_constants.py +5 -0
- package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
- package/hooks/hooks.json +40 -0
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
- package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
- package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
- package/hooks/test__gh_pr_author_swap_utils.py +333 -0
- package/package.json +1 -1
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
- package/skills/bugteam/reference/audit-contract.md +22 -0
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/_claude_permissions_common.py +109 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +51 -2
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +115 -4
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +69 -17
- package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
- package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
- package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
- package/skills/implement/SKILL.md +66 -0
- package/skills/implement/scripts/append_note.py +133 -0
- package/skills/implement/scripts/config/__init__.py +0 -0
- package/skills/implement/scripts/config/notes_constants.py +12 -0
- package/skills/implement/scripts/test_append_note.py +191 -0
- package/skills/pr-converge/SKILL.md +8 -2
- package/skills/pr-converge/config/constants.py +7 -1
- package/skills/pr-converge/reference/state-schema.md +36 -8
- package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/check_convergence.py +167 -28
- package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
- package/skills/pr-converge/scripts/conftest.py +60 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
- package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/test_check_convergence.py +306 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +1 -1
- package/skills/refine/SKILL.md +257 -0
- package/skills/refine/templates/implementation-notes-template.html +56 -0
- package/skills/refine/templates/plan-template.md +60 -0
package/CLAUDE.md
CHANGED
|
@@ -5,7 +5,7 @@ The user delegates execution to you and expects zero manual steps unless strictl
|
|
|
5
5
|
## Code Rules
|
|
6
6
|
@~/.claude/docs/CODE_RULES.md
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
ALWAYS call the AskUserQuestion tool if you have a question for the user. Provide content-appropriate default options, with a flag for the recommended one.
|
|
9
9
|
|
|
10
10
|
## GOTCHAS
|
|
11
11
|
When making code changes, make sure you are working in the proper worktree path for the task at hand.
|
|
@@ -23,6 +23,14 @@ When writing tests, always write tests that actually test the behavior of the fu
|
|
|
23
23
|
|
|
24
24
|
When writing tests, always ensure you utilize the production code paths instead of duplicating explicitly for the test.
|
|
25
25
|
|
|
26
|
+
## Research via Subagents
|
|
27
|
+
|
|
28
|
+
Delegate exploration whose raw content you won't directly edit or reuse. If you'd `Read` more than one file or `Grep` more than one pattern just to extract a fact, dispatch an `Explore` subagent.
|
|
29
|
+
|
|
30
|
+
Ask the subagent for a specific answer: "return the file:line where X is defined." For multiple unrelated questions, fan out parallel subagents — issue several `Agent` calls in a single response.
|
|
31
|
+
|
|
32
|
+
Reserve `Read`/`Grep`/`Glob` for files you will actually touch this turn. Compose subagent prompts via the protocol in `agent-spawn-protocol`.
|
|
33
|
+
|
|
26
34
|
## Additional Non-overlapping Rules
|
|
27
35
|
|
|
28
36
|
- **task_scope:** Match every action to what was explicitly requested. When intent is ambiguous, research official docs and present options via AskUserQuestion before making any changes. Proceed with edits only on explicit instruction.
|
|
@@ -13,6 +13,7 @@ import os
|
|
|
13
13
|
import secrets
|
|
14
14
|
import stat
|
|
15
15
|
import sys
|
|
16
|
+
from collections.abc import Callable
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import NoReturn
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ if str(Path(__file__).resolve().parent) not in sys.path:
|
|
|
21
22
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
23
|
|
|
23
24
|
from config.claude_permissions_constants import (
|
|
25
|
+
ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS,
|
|
24
26
|
CLAUDE_SETTINGS_DIRECTORY_NAME,
|
|
25
27
|
GIT_DIRECTORY_NAME,
|
|
26
28
|
TEXT_FILE_ENCODING as TEXT_FILE_ENCODING,
|
|
@@ -29,11 +31,27 @@ from config.claude_permissions_constants import (
|
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
def exit_with_error(message: str) -> NoReturn:
|
|
34
|
+
"""Print an error message to stderr and terminate the process.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
message: The error message to print to stderr.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
SystemExit: Always raised with a non-zero exit code.
|
|
41
|
+
"""
|
|
32
42
|
print(f"Error: {message}", file=sys.stderr)
|
|
33
43
|
raise SystemExit(1)
|
|
34
44
|
|
|
35
45
|
|
|
36
46
|
def path_contains_glob_metacharacters(candidate_path: str) -> bool:
|
|
47
|
+
"""Check whether a path contains characters reserved for glob patterns.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
candidate_path: The file path string to inspect.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True when any glob metacharacter is present in the path.
|
|
54
|
+
"""
|
|
37
55
|
all_glob_metacharacters_in_path: tuple[str, ...] = (
|
|
38
56
|
"*",
|
|
39
57
|
"?",
|
|
@@ -49,6 +67,14 @@ def path_contains_glob_metacharacters(candidate_path: str) -> bool:
|
|
|
49
67
|
|
|
50
68
|
|
|
51
69
|
def get_current_project_path() -> str:
|
|
70
|
+
"""Return the normalized current working directory path.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The cwd as a POSIX-style path string.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: When the path contains glob metacharacters.
|
|
77
|
+
"""
|
|
52
78
|
normalized_project_path = str(Path.cwd()).replace("\\", "/")
|
|
53
79
|
if path_contains_glob_metacharacters(normalized_project_path):
|
|
54
80
|
raise ValueError(
|
|
@@ -65,13 +91,142 @@ def build_permission_rule(tool_name: str, project_path: str) -> str:
|
|
|
65
91
|
def build_permission_rules(
|
|
66
92
|
project_path: str, all_permission_allow_tools: tuple[str, ...]
|
|
67
93
|
) -> list[str]:
|
|
94
|
+
"""Construct permission rule strings for each tool.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
project_path: The POSIX-style project root path.
|
|
98
|
+
all_permission_allow_tools: Tool names to build rules for.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of permission rule strings for the given project path.
|
|
102
|
+
"""
|
|
68
103
|
return [
|
|
69
104
|
build_permission_rule(each_tool, project_path)
|
|
70
105
|
for each_tool in all_permission_allow_tools
|
|
71
106
|
]
|
|
72
107
|
|
|
73
108
|
|
|
109
|
+
def build_agent_config_deny_rule(
|
|
110
|
+
tool_name: str, project_path: str, agent_config_path_pattern: str
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Construct a deny rule for a single agent-config path pattern.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
tool_name: The permission tool name (e.g., "Edit", "Write", "Read").
|
|
116
|
+
project_path: The POSIX-style project root path.
|
|
117
|
+
agent_config_path_pattern: The agent-config path pattern under .claude/.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The deny rule string Claude Code matches against tool invocations.
|
|
121
|
+
"""
|
|
122
|
+
return f"{tool_name}({project_path}/.claude/{agent_config_path_pattern})"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def build_agent_config_deny_rules(
|
|
126
|
+
project_path: str,
|
|
127
|
+
all_permission_allow_tools: tuple[str, ...],
|
|
128
|
+
all_agent_config_path_patterns: tuple[str, ...],
|
|
129
|
+
) -> list[str]:
|
|
130
|
+
"""Construct deny rules covering every tool and pattern pair.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
project_path: The POSIX-style project root path.
|
|
134
|
+
all_permission_allow_tools: Tool names to build deny rules for.
|
|
135
|
+
all_agent_config_path_patterns: Agent-config path patterns to deny under .claude/.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of deny rule strings, one per tool/pattern combination.
|
|
139
|
+
"""
|
|
140
|
+
return [
|
|
141
|
+
build_agent_config_deny_rule(each_tool, project_path, each_pattern)
|
|
142
|
+
for each_tool in all_permission_allow_tools
|
|
143
|
+
for each_pattern in all_agent_config_path_patterns
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_project_path_token_at_word_boundary(
|
|
148
|
+
body_after_prefix: str, token_position: int
|
|
149
|
+
) -> bool:
|
|
150
|
+
if token_position == 0:
|
|
151
|
+
return True
|
|
152
|
+
preceding_character = body_after_prefix[token_position - 1]
|
|
153
|
+
if preceding_character.isspace():
|
|
154
|
+
return True
|
|
155
|
+
return preceding_character in ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def is_trust_entry_for_project(
|
|
159
|
+
candidate_entry: object, project_path: str, prefix: str
|
|
160
|
+
) -> bool:
|
|
161
|
+
"""Detect whether an autoMode.environment entry is a trust entry for the project.
|
|
162
|
+
|
|
163
|
+
The predicate matches any string entry whose prefix matches the trust-entry
|
|
164
|
+
marker and that contains the project's .claude/** path token anchored on a
|
|
165
|
+
non-path boundary (the start of the body after the prefix, a whitespace
|
|
166
|
+
character, or a quote character). The boundary anchor prevents
|
|
167
|
+
cross-project false positives where the current project's path is a path
|
|
168
|
+
suffix of an unrelated entry's path. The exact wording after the prefix is
|
|
169
|
+
allowed to vary between template revisions.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
candidate_entry: The autoMode.environment list value to inspect.
|
|
173
|
+
project_path: The POSIX-style project root path.
|
|
174
|
+
prefix: The literal prefix that marks a trust entry.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
True when the entry is a prior trust entry for this project.
|
|
178
|
+
"""
|
|
179
|
+
if not isinstance(candidate_entry, str):
|
|
180
|
+
return False
|
|
181
|
+
if not candidate_entry.startswith(prefix):
|
|
182
|
+
return False
|
|
183
|
+
project_path_token = f"{project_path}/.claude/**"
|
|
184
|
+
body_after_prefix = candidate_entry[len(prefix):]
|
|
185
|
+
token_position = body_after_prefix.find(project_path_token)
|
|
186
|
+
while token_position != -1:
|
|
187
|
+
if _is_project_path_token_at_word_boundary(body_after_prefix, token_position):
|
|
188
|
+
return True
|
|
189
|
+
next_search_start = token_position + 1
|
|
190
|
+
token_position = body_after_prefix.find(project_path_token, next_search_start)
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def remove_matching_entries_from_list(
|
|
195
|
+
all_target_list: list[object],
|
|
196
|
+
match_predicate: Callable[[object], bool],
|
|
197
|
+
) -> int:
|
|
198
|
+
"""Remove every entry from a list that satisfies the predicate.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
all_target_list: The list to filter in place.
|
|
202
|
+
match_predicate: Function returning True for entries to remove.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Number of entries removed.
|
|
206
|
+
"""
|
|
207
|
+
original_length = len(all_target_list)
|
|
208
|
+
all_target_list[:] = [
|
|
209
|
+
each_value
|
|
210
|
+
for each_value in all_target_list
|
|
211
|
+
if not match_predicate(each_value)
|
|
212
|
+
]
|
|
213
|
+
return original_length - len(all_target_list)
|
|
214
|
+
|
|
215
|
+
|
|
74
216
|
def load_settings(settings_path: Path) -> dict[str, object]:
|
|
217
|
+
"""Read and parse a JSON settings file from disk.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
settings_path: Path to the JSON settings file.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Parsed settings dictionary. Returns an empty dict when the file
|
|
224
|
+
does not exist.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
SystemExit: When the file exists but is not valid JSON, or when
|
|
228
|
+
its root value is not a JSON object.
|
|
229
|
+
"""
|
|
75
230
|
if not settings_path.exists():
|
|
76
231
|
return {}
|
|
77
232
|
parsed_settings: dict[str, object] = {}
|
|
@@ -96,6 +251,14 @@ def load_settings(settings_path: Path) -> dict[str, object]:
|
|
|
96
251
|
|
|
97
252
|
|
|
98
253
|
def serialize_settings_to_json_text(all_settings: dict[str, object]) -> str:
|
|
254
|
+
"""Serialize a settings dictionary to JSON text with stable formatting.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
all_settings: The settings dictionary to serialize.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Pretty-printed JSON string with sorted keys.
|
|
261
|
+
"""
|
|
99
262
|
json_indent_width_columns: int = len(" ")
|
|
100
263
|
return json.dumps(
|
|
101
264
|
all_settings,
|
|
@@ -105,6 +268,15 @@ def serialize_settings_to_json_text(all_settings: dict[str, object]) -> str:
|
|
|
105
268
|
|
|
106
269
|
|
|
107
270
|
def get_mode_to_preserve(settings_path: Path) -> int:
|
|
271
|
+
"""Return the file permission bits from an existing settings file.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
settings_path: Path to the target settings file to stat.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
The permission bits from the file mode. Returns the default
|
|
278
|
+
secure mode when the file does not exist.
|
|
279
|
+
"""
|
|
108
280
|
default_settings_file_mode: int = 0o600
|
|
109
281
|
try:
|
|
110
282
|
stat_result = os.stat(settings_path)
|
|
@@ -118,6 +290,23 @@ def get_mode_to_preserve(settings_path: Path) -> int:
|
|
|
118
290
|
def write_atomically_with_mode(
|
|
119
291
|
temporary_path: Path, serialized_content: str, file_mode: int
|
|
120
292
|
) -> None:
|
|
293
|
+
"""Create and write to a temporary file with the given mode.
|
|
294
|
+
|
|
295
|
+
Uses os.open with O_CREAT | O_EXCL to create the file securely, then
|
|
296
|
+
writes the serialized content. The caller is responsible for replacing
|
|
297
|
+
the target file with os.replace afterward.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
temporary_path: Path for the temporary file (sibling of target).
|
|
301
|
+
serialized_content: The content to write to the temporary file.
|
|
302
|
+
file_mode: Unix permission bits for the new file.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
OSError: When os.open or os.fdopen fails. The raw file descriptor
|
|
306
|
+
is closed before re-raising so the descriptor does not leak.
|
|
307
|
+
MemoryError: When os.fdopen runs out of buffer memory; the file
|
|
308
|
+
descriptor is closed before re-raising.
|
|
309
|
+
"""
|
|
121
310
|
file_descriptor = os.open(
|
|
122
311
|
str(temporary_path),
|
|
123
312
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
@@ -125,7 +314,7 @@ def write_atomically_with_mode(
|
|
|
125
314
|
)
|
|
126
315
|
try:
|
|
127
316
|
writer = os.fdopen(file_descriptor, "w", encoding=TEXT_FILE_ENCODING)
|
|
128
|
-
except
|
|
317
|
+
except (OSError, MemoryError):
|
|
129
318
|
os.close(file_descriptor)
|
|
130
319
|
raise
|
|
131
320
|
with writer:
|
|
@@ -133,6 +322,15 @@ def write_atomically_with_mode(
|
|
|
133
322
|
|
|
134
323
|
|
|
135
324
|
def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
|
|
325
|
+
"""Write settings to a JSON file atomically with permission preservation.
|
|
326
|
+
|
|
327
|
+
Creates a temporary sibling file, writes content, then atomically
|
|
328
|
+
replaces the target. Cleans up the temporary file in a finally block.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
settings_path: Path to the target settings JSON file.
|
|
332
|
+
all_settings: The settings dictionary to serialize and save.
|
|
333
|
+
"""
|
|
136
334
|
unique_temporary_suffix_byte_length = UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH
|
|
137
335
|
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
138
336
|
serialized_settings = serialize_settings_to_json_text(all_settings)
|
|
@@ -163,6 +361,15 @@ def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
|
|
|
163
361
|
|
|
164
362
|
|
|
165
363
|
def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
|
|
364
|
+
"""Add a value to a list when it is not already present.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
all_target_list: The list to potentially append to.
|
|
368
|
+
new_value: The string value to add when missing.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
True when the value was appended, False when it already existed.
|
|
372
|
+
"""
|
|
166
373
|
if new_value in all_target_list:
|
|
167
374
|
return False
|
|
168
375
|
all_target_list.append(new_value)
|
|
@@ -172,12 +379,19 @@ def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
|
|
|
172
379
|
def ensure_dict_section(
|
|
173
380
|
all_settings: dict[str, object], section_name: str
|
|
174
381
|
) -> dict[str, object]:
|
|
175
|
-
"""Return an existing dict section or create an empty one
|
|
382
|
+
"""Return an existing dict section or create an empty one when absent.
|
|
176
383
|
|
|
177
384
|
A missing key and an explicit JSON null are treated identically: both
|
|
178
385
|
produce a fresh empty dict stored back into settings. Any other non-dict
|
|
179
386
|
value (string, list, number, bool) calls exit_with_error to avoid
|
|
180
387
|
overwriting user data.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
all_settings: The parsed settings dictionary.
|
|
391
|
+
section_name: Key name of the section to retrieve or create.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The existing or newly created section dictionary.
|
|
181
395
|
"""
|
|
182
396
|
existing_section = all_settings.get(section_name)
|
|
183
397
|
if existing_section is None:
|
|
@@ -194,12 +408,19 @@ def ensure_dict_section(
|
|
|
194
408
|
|
|
195
409
|
|
|
196
410
|
def ensure_list_entry(all_section: dict[str, object], entry_name: str) -> list[object]:
|
|
197
|
-
"""Return an existing list entry or create an empty one
|
|
411
|
+
"""Return an existing list entry or create an empty one when absent.
|
|
198
412
|
|
|
199
413
|
A missing key and an explicit JSON null are treated identically: both
|
|
200
414
|
produce a fresh empty list stored back into the section. Any other
|
|
201
415
|
non-list value (string, dict, number, bool) calls exit_with_error to
|
|
202
416
|
avoid overwriting user data.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
all_section: The parent dictionary section.
|
|
420
|
+
entry_name: Key name of the list entry to retrieve or create.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
The existing or newly created list entry.
|
|
203
424
|
"""
|
|
204
425
|
existing_entry = all_section.get(entry_name)
|
|
205
426
|
if existing_entry is None:
|
|
@@ -224,6 +445,13 @@ def is_valid_project_root(candidate_path: Path) -> bool:
|
|
|
224
445
|
def prune_empty_list_then_empty_section(
|
|
225
446
|
all_settings: dict[str, object], section_key: str, list_key: str
|
|
226
447
|
) -> None:
|
|
448
|
+
"""Remove an empty list key and its parent section when both are empty.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
all_settings: The parsed settings dictionary to prune in place.
|
|
452
|
+
section_key: Key of the parent section to check.
|
|
453
|
+
list_key: Key of the list entry within the section.
|
|
454
|
+
"""
|
|
227
455
|
section = all_settings.get(section_key)
|
|
228
456
|
if not isinstance(section, dict):
|
|
229
457
|
return
|
|
@@ -5,7 +5,11 @@ from pathlib import Path
|
|
|
5
5
|
from config.preflight_constants import GIT_DIRECTORY_NAME
|
|
6
6
|
|
|
7
7
|
__all__ = (
|
|
8
|
+
"ALL_AGENT_CONFIG_DENY_TOOLS",
|
|
9
|
+
"ALL_AGENT_CONFIG_PATH_PATTERNS",
|
|
8
10
|
"ALL_PERMISSION_ALLOW_TOOLS",
|
|
11
|
+
"ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS",
|
|
12
|
+
"AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX",
|
|
9
13
|
"AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE",
|
|
10
14
|
"CLAUDE_SETTINGS_DIRECTORY_NAME",
|
|
11
15
|
"CLAUDE_SETTINGS_FILENAME",
|
|
@@ -18,11 +22,61 @@ __all__ = (
|
|
|
18
22
|
|
|
19
23
|
ALL_PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read")
|
|
20
24
|
|
|
25
|
+
ALL_AGENT_CONFIG_DENY_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read", "Glob")
|
|
26
|
+
|
|
27
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS: tuple[str, ...] = (
|
|
28
|
+
"settings*.json",
|
|
29
|
+
"hooks/**",
|
|
30
|
+
"commands/**",
|
|
31
|
+
"agents/**",
|
|
32
|
+
"skills/**",
|
|
33
|
+
"mcp.json",
|
|
34
|
+
"CLAUDE.md",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX: str = "Trusted local workspace:"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _describe_agent_config_pattern_for_humans(agent_config_path_pattern: str) -> str:
|
|
41
|
+
glob_suffix_under_directory = "/**"
|
|
42
|
+
file_name_for_special_phrasing = "mcp.json"
|
|
43
|
+
if agent_config_path_pattern.endswith(glob_suffix_under_directory):
|
|
44
|
+
directory_name = agent_config_path_pattern[
|
|
45
|
+
: -len(glob_suffix_under_directory)
|
|
46
|
+
]
|
|
47
|
+
return f"anything under {directory_name}/"
|
|
48
|
+
if agent_config_path_pattern == file_name_for_special_phrasing:
|
|
49
|
+
return f"the {file_name_for_special_phrasing} file"
|
|
50
|
+
return agent_config_path_pattern
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _build_agent_config_pattern_phrase(
|
|
54
|
+
all_agent_config_path_patterns: tuple[str, ...],
|
|
55
|
+
) -> str:
|
|
56
|
+
all_described_patterns: list[str] = [
|
|
57
|
+
_describe_agent_config_pattern_for_humans(each_pattern)
|
|
58
|
+
for each_pattern in all_agent_config_path_patterns
|
|
59
|
+
]
|
|
60
|
+
if len(all_described_patterns) <= 1:
|
|
61
|
+
return ", ".join(all_described_patterns)
|
|
62
|
+
leading_phrase_parts = ", ".join(all_described_patterns[:-1])
|
|
63
|
+
final_phrase_part = all_described_patterns[-1]
|
|
64
|
+
return f"{leading_phrase_parts}, and {final_phrase_part}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_AGENT_CONFIG_PATTERN_PHRASE: str = _build_agent_config_pattern_phrase(
|
|
68
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS
|
|
69
|
+
)
|
|
70
|
+
|
|
21
71
|
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE: str = (
|
|
22
|
-
"Trusted local workspace: {project_path}/.claude/**
|
|
23
|
-
"
|
|
72
|
+
f"Trusted local workspace: Files under {{project_path}}/.claude/** inherit "
|
|
73
|
+
f"the workspace's trust for Edit, Write, Read, and Glob operations EXCEPT "
|
|
74
|
+
f"for agent-config files: {_AGENT_CONFIG_PATTERN_PHRASE}. Edits to those "
|
|
75
|
+
f"agent-config files always require explicit per-edit user approval."
|
|
24
76
|
)
|
|
25
77
|
|
|
78
|
+
ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS: tuple[str, ...] = ('"', "'")
|
|
79
|
+
|
|
26
80
|
CLAUDE_SETTINGS_DIRECTORY_NAME: str = ".claude"
|
|
27
81
|
|
|
28
82
|
CLAUDE_SETTINGS_FILENAME: str = "settings.json"
|
|
@@ -4,6 +4,8 @@ CLAUDE_SETTINGS_PERMISSIONS_KEY: str = "permissions"
|
|
|
4
4
|
|
|
5
5
|
CLAUDE_SETTINGS_ALLOW_KEY: str = "allow"
|
|
6
6
|
|
|
7
|
+
CLAUDE_SETTINGS_DENY_KEY: str = "deny"
|
|
8
|
+
|
|
7
9
|
CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY: str = "additionalDirectories"
|
|
8
10
|
|
|
9
11
|
CLAUDE_SETTINGS_AUTO_MODE_KEY: str = "autoMode"
|