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.
Files changed (66) hide show
  1. package/CLAUDE.md +9 -1
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +231 -3
  3. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +56 -2
  4. package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +2 -0
  5. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +173 -6
  6. package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
  7. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +135 -14
  8. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +33 -0
  10. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
  11. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +4 -2
  12. package/hooks/_gh_pr_author_swap_utils.py +1211 -0
  13. package/hooks/blocking/gh_body_arg_blocker.py +9 -6
  14. package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
  15. package/hooks/blocking/gh_pr_author_restore.py +100 -0
  16. package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
  17. package/hooks/blocking/pr_description_enforcer.py +1 -3
  18. package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
  19. package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
  20. package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
  21. package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
  22. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
  23. package/hooks/config/gh_pr_author_swap_constants.py +76 -0
  24. package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
  25. package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
  26. package/hooks/config/pr_description_enforcer_constants.py +5 -0
  27. package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
  28. package/hooks/hooks.json +40 -0
  29. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
  30. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
  31. package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
  32. package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
  33. package/hooks/test__gh_pr_author_swap_utils.py +333 -0
  34. package/package.json +1 -1
  35. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  36. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  37. package/skills/bugteam/reference/audit-contract.md +22 -0
  38. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  39. package/skills/bugteam/scripts/_claude_permissions_common.py +109 -0
  40. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
  41. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +51 -2
  42. package/skills/bugteam/scripts/grant_project_claude_permissions.py +115 -4
  43. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +69 -17
  44. package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
  45. package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
  46. package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
  47. package/skills/implement/SKILL.md +66 -0
  48. package/skills/implement/scripts/append_note.py +133 -0
  49. package/skills/implement/scripts/config/__init__.py +0 -0
  50. package/skills/implement/scripts/config/notes_constants.py +12 -0
  51. package/skills/implement/scripts/test_append_note.py +191 -0
  52. package/skills/pr-converge/SKILL.md +8 -2
  53. package/skills/pr-converge/config/constants.py +7 -1
  54. package/skills/pr-converge/reference/state-schema.md +36 -8
  55. package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
  56. package/skills/pr-converge/scripts/check_convergence.py +167 -28
  57. package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
  58. package/skills/pr-converge/scripts/conftest.py +60 -0
  59. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
  60. package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
  61. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
  62. package/skills/pr-converge/scripts/test_check_convergence.py +306 -0
  63. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +1 -1
  64. package/skills/refine/SKILL.md +257 -0
  65. package/skills/refine/templates/implementation-notes-template.html +56 -0
  66. 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
- When a UNC path is mapped to a drive letter, ALWAYS prefer the drive letter and NEVER use UNC.
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 BaseException:
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 if absent.
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 if absent.
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/** is the user's "
23
- "project Claude Code config tree; edits inside are routine"
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"