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
|
@@ -9,23 +9,42 @@ the changes applied. No-op when the entries already exist.
|
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
sys.path.
|
|
12
|
+
parent_directory = str(Path(__file__).absolute().parent)
|
|
13
|
+
try:
|
|
14
|
+
sys.path.remove(parent_directory)
|
|
15
|
+
except ValueError:
|
|
16
|
+
pass
|
|
17
|
+
if parent_directory not in sys.path:
|
|
18
|
+
sys.path.insert(0, parent_directory)
|
|
19
|
+
|
|
20
|
+
for each_cached_module_name in [
|
|
21
|
+
each_module_key
|
|
22
|
+
for each_module_key in list(sys.modules)
|
|
23
|
+
if each_module_key == "config"
|
|
24
|
+
or each_module_key.startswith("config.")
|
|
25
|
+
or each_module_key == "_claude_permissions_common"
|
|
26
|
+
]:
|
|
27
|
+
sys.modules.pop(each_cached_module_name, None)
|
|
15
28
|
|
|
16
29
|
from _claude_permissions_common import ( # noqa: E402
|
|
17
30
|
append_if_missing,
|
|
31
|
+
build_agent_config_deny_rules,
|
|
18
32
|
build_permission_rules,
|
|
19
33
|
ensure_dict_section,
|
|
20
34
|
ensure_list_entry,
|
|
21
35
|
exit_with_error,
|
|
22
36
|
get_current_project_path,
|
|
37
|
+
is_trust_entry_for_project,
|
|
23
38
|
is_valid_project_root,
|
|
24
39
|
load_settings,
|
|
40
|
+
remove_matching_entries_from_list,
|
|
25
41
|
save_settings,
|
|
26
42
|
)
|
|
27
43
|
from config.claude_permissions_constants import ( # noqa: E402
|
|
44
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
45
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
28
46
|
ALL_PERMISSION_ALLOW_TOOLS,
|
|
47
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
|
|
29
48
|
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
|
|
30
49
|
get_claude_user_settings_path,
|
|
31
50
|
)
|
|
@@ -33,6 +52,7 @@ from config.claude_settings_keys_constants import ( # noqa: E402
|
|
|
33
52
|
CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
|
|
34
53
|
CLAUDE_SETTINGS_ALLOW_KEY,
|
|
35
54
|
CLAUDE_SETTINGS_AUTO_MODE_KEY,
|
|
55
|
+
CLAUDE_SETTINGS_DENY_KEY,
|
|
36
56
|
CLAUDE_SETTINGS_ENVIRONMENT_KEY,
|
|
37
57
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
38
58
|
)
|
|
@@ -41,6 +61,15 @@ from config.claude_settings_keys_constants import ( # noqa: E402
|
|
|
41
61
|
def add_rules_to_allow_list(
|
|
42
62
|
all_settings: dict[str, object], all_rules_to_add: list[str]
|
|
43
63
|
) -> int:
|
|
64
|
+
"""Add permission rules to the settings allow list.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
all_settings: The parsed settings dictionary.
|
|
68
|
+
all_rules_to_add: Permission rule strings to append.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Number of rules actually added (new entries).
|
|
72
|
+
"""
|
|
44
73
|
permissions_section = ensure_dict_section(
|
|
45
74
|
all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
|
|
46
75
|
)
|
|
@@ -54,9 +83,47 @@ def add_rules_to_allow_list(
|
|
|
54
83
|
)
|
|
55
84
|
|
|
56
85
|
|
|
86
|
+
def add_rules_to_deny_list(
|
|
87
|
+
all_settings: dict[str, object], all_rules_to_add: list[str]
|
|
88
|
+
) -> int:
|
|
89
|
+
"""Add permission rules to the settings deny list.
|
|
90
|
+
|
|
91
|
+
Deny rules take precedence over allow rules in Claude Code's permission
|
|
92
|
+
matching, so writing agent-config paths into the deny list forces a
|
|
93
|
+
per-edit user approval even when a broader allow rule would cover them.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
all_settings: The parsed settings dictionary.
|
|
97
|
+
all_rules_to_add: Permission rule strings to append.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Number of rules actually added (new entries).
|
|
101
|
+
"""
|
|
102
|
+
permissions_section = ensure_dict_section(
|
|
103
|
+
all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
|
|
104
|
+
)
|
|
105
|
+
existing_deny_list = ensure_list_entry(
|
|
106
|
+
permissions_section, CLAUDE_SETTINGS_DENY_KEY
|
|
107
|
+
)
|
|
108
|
+
return sum(
|
|
109
|
+
1
|
|
110
|
+
for each_rule in all_rules_to_add
|
|
111
|
+
if append_if_missing(existing_deny_list, each_rule)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
57
115
|
def add_directory_to_additional_directories(
|
|
58
116
|
all_settings: dict[str, object], directory_path: str
|
|
59
117
|
) -> int:
|
|
118
|
+
"""Add a project path to the additionalDirectories allow list.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
all_settings: The parsed settings dictionary.
|
|
122
|
+
directory_path: The project directory path to add.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
1 when the entry was added, 0 when it already existed.
|
|
126
|
+
"""
|
|
60
127
|
permissions_section = ensure_dict_section(
|
|
61
128
|
all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
|
|
62
129
|
)
|
|
@@ -71,6 +138,15 @@ def add_directory_to_additional_directories(
|
|
|
71
138
|
def add_auto_mode_environment_entry(
|
|
72
139
|
all_settings: dict[str, object], entry_text: str
|
|
73
140
|
) -> int:
|
|
141
|
+
"""Add an auto-mode environment entry for the project.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
all_settings: The parsed settings dictionary.
|
|
145
|
+
entry_text: The environment entry text to add.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
1 when the entry was added, 0 when it already existed.
|
|
149
|
+
"""
|
|
74
150
|
auto_mode_section = ensure_dict_section(
|
|
75
151
|
all_settings, CLAUDE_SETTINGS_AUTO_MODE_KEY
|
|
76
152
|
)
|
|
@@ -82,7 +158,69 @@ def add_auto_mode_environment_entry(
|
|
|
82
158
|
return 0
|
|
83
159
|
|
|
84
160
|
|
|
161
|
+
def purge_stale_trust_entries(
|
|
162
|
+
all_settings: dict[str, object],
|
|
163
|
+
project_path: str,
|
|
164
|
+
prefix: str,
|
|
165
|
+
protected_entry: str | None = None,
|
|
166
|
+
) -> int:
|
|
167
|
+
"""Remove every prior trust entry for the project from autoMode.environment.
|
|
168
|
+
|
|
169
|
+
A trust entry is any string in autoMode.environment whose prefix matches
|
|
170
|
+
the trust-entry marker and that contains the project's .claude/** path.
|
|
171
|
+
Purging stale entries before adding the current template prevents
|
|
172
|
+
accumulation across template revisions. The optional protected_entry
|
|
173
|
+
survives the purge so an entry byte-identical to the one about to be
|
|
174
|
+
re-added is not removed and re-added on every invocation, preserving the
|
|
175
|
+
idempotency contract documented on grant_permissions_for_current_directory.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
all_settings: The parsed settings dictionary.
|
|
179
|
+
project_path: The POSIX-style project root path.
|
|
180
|
+
prefix: The literal prefix that marks a trust entry.
|
|
181
|
+
protected_entry: Optional entry text that, when byte-equal to a
|
|
182
|
+
candidate, prevents removal. Pass the freshly-formatted current
|
|
183
|
+
template entry from grant to preserve idempotency. Revoke passes
|
|
184
|
+
None so every matching entry is removed.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Number of stale entries removed.
|
|
188
|
+
"""
|
|
189
|
+
auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
|
|
190
|
+
if not isinstance(auto_mode_section, dict):
|
|
191
|
+
return 0
|
|
192
|
+
existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
|
|
193
|
+
if not isinstance(existing_environment, list):
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
def _should_purge_candidate(candidate_entry: object) -> bool:
|
|
197
|
+
if not is_trust_entry_for_project(candidate_entry, project_path, prefix):
|
|
198
|
+
return False
|
|
199
|
+
if protected_entry is not None and candidate_entry == protected_entry:
|
|
200
|
+
return False
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
return remove_matching_entries_from_list(
|
|
204
|
+
existing_environment,
|
|
205
|
+
_should_purge_candidate,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
85
209
|
def grant_permissions_for_current_directory() -> None:
|
|
210
|
+
"""Grant Edit/Write/Read permissions for the current project directory.
|
|
211
|
+
|
|
212
|
+
Reads the current project path, constructs permission rules from config
|
|
213
|
+
constants, and writes them to ~/.claude/settings.json atomically. Adds
|
|
214
|
+
deny rules for agent-config paths so edits to settings, hooks, commands,
|
|
215
|
+
agents, skills, mcp.json, and CLAUDE.md still require per-edit user
|
|
216
|
+
approval. Purges any prior trust entries for this project before writing
|
|
217
|
+
the current template to prevent accumulation across template revisions.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
SystemExit: When the current directory is not a valid project root.
|
|
221
|
+
ValueError: Propagated from get_current_project_path() when the path
|
|
222
|
+
contains glob metacharacters.
|
|
223
|
+
"""
|
|
86
224
|
claude_user_settings_path: Path = get_claude_user_settings_path()
|
|
87
225
|
project_root_path = Path.cwd()
|
|
88
226
|
if not is_valid_project_root(project_root_path):
|
|
@@ -96,19 +234,37 @@ def grant_permissions_for_current_directory() -> None:
|
|
|
96
234
|
all_permission_rules = build_permission_rules(
|
|
97
235
|
project_path, ALL_PERMISSION_ALLOW_TOOLS
|
|
98
236
|
)
|
|
237
|
+
all_agent_config_deny_rules = build_agent_config_deny_rules(
|
|
238
|
+
project_path,
|
|
239
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
240
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
241
|
+
)
|
|
99
242
|
environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
|
|
100
243
|
project_path=project_path
|
|
101
244
|
)
|
|
102
245
|
settings = load_settings(claude_user_settings_path)
|
|
103
|
-
|
|
246
|
+
allow_rules_added_count = add_rules_to_allow_list(settings, all_permission_rules)
|
|
247
|
+
deny_rules_added_count = add_rules_to_deny_list(
|
|
248
|
+
settings, all_agent_config_deny_rules
|
|
249
|
+
)
|
|
104
250
|
directories_added_count = add_directory_to_additional_directories(
|
|
105
251
|
settings, project_path
|
|
106
252
|
)
|
|
253
|
+
stale_trust_entries_purged_count = purge_stale_trust_entries(
|
|
254
|
+
settings,
|
|
255
|
+
project_path,
|
|
256
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
|
|
257
|
+
protected_entry=environment_entry,
|
|
258
|
+
)
|
|
107
259
|
environment_entries_added_count = add_auto_mode_environment_entry(
|
|
108
260
|
settings, environment_entry
|
|
109
261
|
)
|
|
110
262
|
total_changes_count = (
|
|
111
|
-
|
|
263
|
+
allow_rules_added_count
|
|
264
|
+
+ deny_rules_added_count
|
|
265
|
+
+ directories_added_count
|
|
266
|
+
+ stale_trust_entries_purged_count
|
|
267
|
+
+ environment_entries_added_count
|
|
112
268
|
)
|
|
113
269
|
if total_changes_count == 0:
|
|
114
270
|
print(f"Project path: {project_path}")
|
|
@@ -118,8 +274,19 @@ def grant_permissions_for_current_directory() -> None:
|
|
|
118
274
|
save_settings(claude_user_settings_path, settings)
|
|
119
275
|
print(f"Project path: {project_path}")
|
|
120
276
|
print(f"Settings file: {claude_user_settings_path}")
|
|
121
|
-
print(
|
|
277
|
+
print(
|
|
278
|
+
f"Allow rules added: {allow_rules_added_count} of {len(all_permission_rules)}"
|
|
279
|
+
)
|
|
280
|
+
print(
|
|
281
|
+
f"Deny rules added: {deny_rules_added_count} of "
|
|
282
|
+
f"{len(all_agent_config_deny_rules)}"
|
|
283
|
+
)
|
|
122
284
|
print(f"Additional directories added: {directories_added_count}")
|
|
285
|
+
if stale_trust_entries_purged_count > 0:
|
|
286
|
+
print(
|
|
287
|
+
f"Stale auto-mode environment entries purged: "
|
|
288
|
+
f"{stale_trust_entries_purged_count}"
|
|
289
|
+
)
|
|
123
290
|
print(f"Auto-mode environment entries added: {environment_entries_added_count}")
|
|
124
291
|
|
|
125
292
|
|
|
@@ -33,8 +33,8 @@ from pathlib import Path
|
|
|
33
33
|
from typing import NoReturn
|
|
34
34
|
|
|
35
35
|
sys.modules.pop("config", None)
|
|
36
|
-
if str(Path(__file__).
|
|
37
|
-
sys.path.insert(0, str(Path(__file__).
|
|
36
|
+
if str(Path(__file__).absolute().parent) not in sys.path:
|
|
37
|
+
sys.path.insert(0, str(Path(__file__).absolute().parent))
|
|
38
38
|
|
|
39
39
|
from config.post_audit_thread_constants import (
|
|
40
40
|
ALL_GH_API_COMMAND_PARTS,
|
|
@@ -10,28 +10,47 @@ autoMode sections so repeated grant/revoke cycles leave no dead structure.
|
|
|
10
10
|
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
sys.path.
|
|
13
|
+
parent_directory = str(Path(__file__).absolute().parent)
|
|
14
|
+
try:
|
|
15
|
+
sys.path.remove(parent_directory)
|
|
16
|
+
except ValueError:
|
|
17
|
+
pass
|
|
18
|
+
if parent_directory not in sys.path:
|
|
19
|
+
sys.path.insert(0, parent_directory)
|
|
20
|
+
|
|
21
|
+
for each_cached_module_name in [
|
|
22
|
+
each_module_key
|
|
23
|
+
for each_module_key in list(sys.modules)
|
|
24
|
+
if each_module_key == "config"
|
|
25
|
+
or each_module_key.startswith("config.")
|
|
26
|
+
or each_module_key == "_claude_permissions_common"
|
|
27
|
+
]:
|
|
28
|
+
sys.modules.pop(each_cached_module_name, None)
|
|
16
29
|
|
|
17
30
|
from _claude_permissions_common import ( # noqa: E402
|
|
31
|
+
build_agent_config_deny_rules,
|
|
18
32
|
build_permission_rules,
|
|
19
33
|
exit_with_error,
|
|
20
34
|
get_current_project_path,
|
|
35
|
+
is_trust_entry_for_project,
|
|
21
36
|
is_valid_project_root,
|
|
22
37
|
load_settings,
|
|
23
38
|
prune_empty_list_then_empty_section,
|
|
39
|
+
remove_matching_entries_from_list,
|
|
24
40
|
save_settings,
|
|
25
41
|
)
|
|
26
42
|
from config.claude_permissions_constants import ( # noqa: E402
|
|
43
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
44
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
27
45
|
ALL_PERMISSION_ALLOW_TOOLS,
|
|
28
|
-
|
|
46
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
|
|
29
47
|
get_claude_user_settings_path,
|
|
30
48
|
)
|
|
31
49
|
from config.claude_settings_keys_constants import ( # noqa: E402
|
|
32
50
|
CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
|
|
33
51
|
CLAUDE_SETTINGS_ALLOW_KEY,
|
|
34
52
|
CLAUDE_SETTINGS_AUTO_MODE_KEY,
|
|
53
|
+
CLAUDE_SETTINGS_DENY_KEY,
|
|
35
54
|
CLAUDE_SETTINGS_ENVIRONMENT_KEY,
|
|
36
55
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
37
56
|
)
|
|
@@ -40,6 +59,15 @@ from config.claude_settings_keys_constants import ( # noqa: E402
|
|
|
40
59
|
def remove_values_from_list(
|
|
41
60
|
all_target_list: list[object], all_values_to_remove: set[str]
|
|
42
61
|
) -> int:
|
|
62
|
+
"""Remove matching values from a list in place.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
all_target_list: The list to remove values from.
|
|
66
|
+
all_values_to_remove: Set of string values to remove.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Number of values removed.
|
|
70
|
+
"""
|
|
43
71
|
original_length = len(all_target_list)
|
|
44
72
|
all_target_list[:] = [
|
|
45
73
|
each_value
|
|
@@ -52,6 +80,15 @@ def remove_values_from_list(
|
|
|
52
80
|
def remove_rules_from_allow_list(
|
|
53
81
|
all_settings: dict[str, object], all_rules_to_remove: list[str]
|
|
54
82
|
) -> int:
|
|
83
|
+
"""Remove matching permission rules from the settings allow list.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
all_settings: The parsed settings dictionary.
|
|
87
|
+
all_rules_to_remove: Permission rule strings to remove.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Number of rules removed.
|
|
91
|
+
"""
|
|
55
92
|
permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
|
|
56
93
|
if not isinstance(permissions_section, dict):
|
|
57
94
|
return 0
|
|
@@ -61,9 +98,39 @@ def remove_rules_from_allow_list(
|
|
|
61
98
|
return remove_values_from_list(existing_allow_list, set(all_rules_to_remove))
|
|
62
99
|
|
|
63
100
|
|
|
101
|
+
def remove_rules_from_deny_list(
|
|
102
|
+
all_settings: dict[str, object], all_rules_to_remove: list[str]
|
|
103
|
+
) -> int:
|
|
104
|
+
"""Remove matching permission rules from the settings deny list.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
all_settings: The parsed settings dictionary.
|
|
108
|
+
all_rules_to_remove: Permission rule strings to remove.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Number of rules removed.
|
|
112
|
+
"""
|
|
113
|
+
permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
|
|
114
|
+
if not isinstance(permissions_section, dict):
|
|
115
|
+
return 0
|
|
116
|
+
existing_deny_list = permissions_section.get(CLAUDE_SETTINGS_DENY_KEY)
|
|
117
|
+
if not isinstance(existing_deny_list, list):
|
|
118
|
+
return 0
|
|
119
|
+
return remove_values_from_list(existing_deny_list, set(all_rules_to_remove))
|
|
120
|
+
|
|
121
|
+
|
|
64
122
|
def remove_directory_from_additional_directories(
|
|
65
123
|
all_settings: dict[str, object], directory_path: str
|
|
66
124
|
) -> int:
|
|
125
|
+
"""Remove a project path from the additionalDirectories list.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
all_settings: The parsed settings dictionary.
|
|
129
|
+
directory_path: The project directory path to remove.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
1 when the entry was removed, 0 when not found.
|
|
133
|
+
"""
|
|
67
134
|
permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
|
|
68
135
|
if not isinstance(permissions_section, dict):
|
|
69
136
|
return 0
|
|
@@ -75,24 +142,54 @@ def remove_directory_from_additional_directories(
|
|
|
75
142
|
return remove_values_from_list(existing_directories, {directory_path})
|
|
76
143
|
|
|
77
144
|
|
|
78
|
-
def
|
|
79
|
-
all_settings: dict[str, object],
|
|
145
|
+
def remove_trust_entries_for_project(
|
|
146
|
+
all_settings: dict[str, object], project_path: str, prefix: str
|
|
80
147
|
) -> int:
|
|
148
|
+
"""Remove every trust entry for the project from autoMode.environment.
|
|
149
|
+
|
|
150
|
+
Matches any string in autoMode.environment whose prefix matches the
|
|
151
|
+
trust-entry marker and that contains the project's .claude/** path.
|
|
152
|
+
The match is wording-agnostic so prior template revisions are removed
|
|
153
|
+
cleanly even when the current template differs.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
all_settings: The parsed settings dictionary.
|
|
157
|
+
project_path: The POSIX-style project root path.
|
|
158
|
+
prefix: The literal prefix that marks a trust entry.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Number of entries removed.
|
|
162
|
+
"""
|
|
81
163
|
auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
|
|
82
164
|
if not isinstance(auto_mode_section, dict):
|
|
83
165
|
return 0
|
|
84
166
|
existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
|
|
85
167
|
if not isinstance(existing_environment, list):
|
|
86
168
|
return 0
|
|
87
|
-
return
|
|
169
|
+
return remove_matching_entries_from_list(
|
|
170
|
+
existing_environment,
|
|
171
|
+
lambda candidate_entry: is_trust_entry_for_project(
|
|
172
|
+
candidate_entry, project_path, prefix
|
|
173
|
+
),
|
|
174
|
+
)
|
|
88
175
|
|
|
89
176
|
|
|
90
177
|
def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
178
|
+
"""Remove empty lists and their parent sections after revoking entries.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
all_settings: The parsed settings dictionary to prune in place.
|
|
182
|
+
"""
|
|
91
183
|
prune_empty_list_then_empty_section(
|
|
92
184
|
all_settings,
|
|
93
185
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
94
186
|
CLAUDE_SETTINGS_ALLOW_KEY,
|
|
95
187
|
)
|
|
188
|
+
prune_empty_list_then_empty_section(
|
|
189
|
+
all_settings,
|
|
190
|
+
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
191
|
+
CLAUDE_SETTINGS_DENY_KEY,
|
|
192
|
+
)
|
|
96
193
|
prune_empty_list_then_empty_section(
|
|
97
194
|
all_settings,
|
|
98
195
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
@@ -106,6 +203,18 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
|
106
203
|
|
|
107
204
|
|
|
108
205
|
def revoke_permissions_for_current_directory() -> None:
|
|
206
|
+
"""Revoke permissions previously granted for the current project directory.
|
|
207
|
+
|
|
208
|
+
Reads the current project path, constructs the matching allow and deny
|
|
209
|
+
permission rules, removes them from ~/.claude/settings.json, removes
|
|
210
|
+
every trust entry for the project from autoMode.environment, and prunes
|
|
211
|
+
any newly empty sections.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
SystemExit: When the current directory is not a valid project root.
|
|
215
|
+
ValueError: Propagated from get_current_project_path() when the path
|
|
216
|
+
contains glob metacharacters.
|
|
217
|
+
"""
|
|
109
218
|
claude_user_settings_path: Path = get_claude_user_settings_path()
|
|
110
219
|
project_root_path = Path.cwd()
|
|
111
220
|
if not is_valid_project_root(project_root_path):
|
|
@@ -117,19 +226,25 @@ def revoke_permissions_for_current_directory() -> None:
|
|
|
117
226
|
raise SystemExit(1)
|
|
118
227
|
project_path = get_current_project_path()
|
|
119
228
|
permission_rules = build_permission_rules(project_path, ALL_PERMISSION_ALLOW_TOOLS)
|
|
120
|
-
|
|
121
|
-
project_path
|
|
229
|
+
all_agent_config_deny_rules = build_agent_config_deny_rules(
|
|
230
|
+
project_path,
|
|
231
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
232
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
122
233
|
)
|
|
123
234
|
settings = load_settings(claude_user_settings_path)
|
|
124
|
-
|
|
235
|
+
allow_rules_removed_count = remove_rules_from_allow_list(settings, permission_rules)
|
|
236
|
+
deny_rules_removed_count = remove_rules_from_deny_list(
|
|
237
|
+
settings, all_agent_config_deny_rules
|
|
238
|
+
)
|
|
125
239
|
directories_removed_count = remove_directory_from_additional_directories(
|
|
126
240
|
settings, project_path
|
|
127
241
|
)
|
|
128
|
-
environment_entries_removed_count =
|
|
129
|
-
settings,
|
|
242
|
+
environment_entries_removed_count = remove_trust_entries_for_project(
|
|
243
|
+
settings, project_path, AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX
|
|
130
244
|
)
|
|
131
245
|
total_changes_count = (
|
|
132
|
-
|
|
246
|
+
allow_rules_removed_count
|
|
247
|
+
+ deny_rules_removed_count
|
|
133
248
|
+ directories_removed_count
|
|
134
249
|
+ environment_entries_removed_count
|
|
135
250
|
)
|
|
@@ -142,7 +257,13 @@ def revoke_permissions_for_current_directory() -> None:
|
|
|
142
257
|
save_settings(claude_user_settings_path, settings)
|
|
143
258
|
print(f"Project path: {project_path}")
|
|
144
259
|
print(f"Settings file: {claude_user_settings_path}")
|
|
145
|
-
print(
|
|
260
|
+
print(
|
|
261
|
+
f"Allow rules removed: {allow_rules_removed_count} of {len(permission_rules)}"
|
|
262
|
+
)
|
|
263
|
+
print(
|
|
264
|
+
f"Deny rules removed: {deny_rules_removed_count} of "
|
|
265
|
+
f"{len(all_agent_config_deny_rules)}"
|
|
266
|
+
)
|
|
146
267
|
print(f"Additional directories removed: {directories_removed_count}")
|
|
147
268
|
print(
|
|
148
269
|
f"Auto-mode environment entries removed: {environment_entries_removed_count}"
|