claude-dev-env 1.41.0 → 1.43.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 +8 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +232 -8
- package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
- package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +124 -20
- package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +90 -0
- package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +2 -0
- package/_shared/pr-loop/scripts/preflight.py +13 -31
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +76 -33
- package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +37 -2
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +5 -3
- package/agents/pr-description-writer.md +50 -140
- package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
- package/hooks/_gh_pr_author_swap_utils.py +1 -1
- package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
- package/hooks/blocking/code_rules_enforcer.py +217 -99
- package/hooks/blocking/code_rules_path_utils.py +8 -1
- package/hooks/blocking/destructive_command_blocker.py +1 -1
- package/hooks/blocking/es_exe_path_rewriter.py +7 -13
- package/hooks/blocking/gh_body_arg_blocker.py +6 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
- package/hooks/blocking/gh_pr_author_restore.py +5 -5
- package/hooks/blocking/hedging_language_blocker.py +4 -10
- package/hooks/blocking/md_path_exemptions.py +205 -0
- package/hooks/blocking/md_to_html_blocker.py +48 -20
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
- package/hooks/blocking/pr_description_enforcer.py +626 -41
- package/hooks/blocking/question_to_user_enforcer.py +4 -10
- package/hooks/blocking/state_description_blocker.py +6 -12
- package/hooks/blocking/tdd_enforcer.py +1 -1
- package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +3 -3
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
- package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
- package/hooks/blocking/test_hedging_language_blocker.py +2 -2
- package/hooks/blocking/test_md_to_html_blocker.py +463 -8
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
- package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
- package/hooks/blocking/windows_rmtree_blocker.py +5 -11
- package/hooks/diagnostic/hook_log_extractor.py +1 -1
- package/hooks/diagnostic/hook_log_init.py +1 -1
- package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
- package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
- package/hooks/diagnostic/test_hook_log_init.py +2 -2
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
- package/hooks/git-hooks/gate_utils.py +1 -1
- package/hooks/git-hooks/pre_commit.py +1 -1
- package/hooks/git-hooks/pre_push.py +1 -1
- package/hooks/git-hooks/test_config.py +5 -5
- package/hooks/git-hooks/test_pre_push.py +6 -6
- package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
- package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
- package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
- package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
- package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
- package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
- package/hooks/{config → hooks_constants}/test_messages.py +2 -6
- package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
- package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
- package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
- package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
- package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
- package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
- package/hooks/session/session_env_cleanup.py +4 -10
- package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
- package/hooks/session/test_untracked_repo_detector.py +2 -2
- package/hooks/session/untracked_repo_detector.py +6 -12
- package/hooks/test__gh_pr_author_swap_utils.py +1 -1
- package/hooks/validators/run_all_validators.py +16 -5
- package/hooks/validators/test_output_formatter.py +46 -0
- package/hooks/workflow/doc_gist_auto_publish.py +1 -1
- package/hooks/workflow/md_to_html_companion.py +8 -15
- package/hooks/workflow/test_md_to_html_companion.py +184 -23
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +1 -1
- package/rules/vault-context.md +1 -1
- package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
- package/scripts/setup_project_paths.py +49 -11
- package/scripts/sweep_empty_dirs.py +10 -1
- package/scripts/test_setup_project_paths.py +2 -2
- package/scripts/test_sweep_empty_dirs.py +2 -6
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +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/PROMPTS.md +1 -1
- package/skills/bugteam/SKILL.md +1 -1
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +110 -13
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
- package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
- package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +69 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +117 -12
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +71 -25
- package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
- package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
- package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
- package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
- package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
- package/skills/doc-gist/SKILL.md +1 -1
- package/skills/doc-gist/scripts/gist_upload.py +1 -1
- package/skills/implement/SKILL.md +66 -0
- package/skills/implement/scripts/append_note.py +133 -0
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +12 -0
- package/skills/implement/scripts/test_append_note.py +191 -0
- package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
- package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +6 -1
- package/skills/pr-converge/scripts/check_bugbot_ci.py +2 -2
- package/skills/pr-converge/scripts/check_convergence.py +175 -29
- package/skills/pr-converge/scripts/check_pending_reviews.py +2 -2
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +2 -2
- package/skills/pr-converge/scripts/post_fix_reply.py +2 -2
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
- package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
- package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
- package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/test_check_convergence.py +324 -0
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
- 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/skills/session-log/SKILL.md +98 -233
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +0 -36
- package/hooks/config/pr_description_enforcer_constants.py +0 -19
- package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
- package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +0 -20
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
- /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
- /package/hooks/{config → hooks_constants}/__init__.py +0 -0
- /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
- /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
- /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
- /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/messages.py +0 -0
- /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
- /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
- /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
- /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
- /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
- /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
- /package/skills/{pr-converge/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
- /package/skills/{pr-converge/scripts/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
- /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
- /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
|
@@ -9,43 +9,37 @@ the changes applied. No-op when the entries already exist.
|
|
|
9
9
|
import sys
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
-
parent_directory = str(Path(__file__).
|
|
13
|
-
try:
|
|
14
|
-
sys.path.remove(parent_directory)
|
|
15
|
-
except ValueError:
|
|
16
|
-
pass
|
|
12
|
+
parent_directory = str(Path(__file__).resolve().parent)
|
|
17
13
|
if parent_directory not in sys.path:
|
|
18
14
|
sys.path.insert(0, parent_directory)
|
|
19
15
|
|
|
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)
|
|
28
|
-
|
|
29
16
|
from _claude_permissions_common import ( # noqa: E402
|
|
30
17
|
append_if_missing,
|
|
18
|
+
build_agent_config_deny_rules,
|
|
31
19
|
build_permission_rules,
|
|
32
20
|
ensure_dict_section,
|
|
33
21
|
ensure_list_entry,
|
|
34
22
|
exit_with_error,
|
|
35
23
|
get_current_project_path,
|
|
24
|
+
is_trust_entry_for_project,
|
|
36
25
|
is_valid_project_root,
|
|
37
26
|
load_settings,
|
|
27
|
+
remove_matching_entries_from_list,
|
|
38
28
|
save_settings,
|
|
39
29
|
)
|
|
40
|
-
from
|
|
30
|
+
from pr_loop_shared_constants.claude_permissions_constants import (
|
|
31
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
32
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
41
33
|
ALL_PERMISSION_ALLOW_TOOLS,
|
|
34
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
|
|
42
35
|
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
|
|
43
36
|
get_claude_user_settings_path,
|
|
44
37
|
)
|
|
45
|
-
from
|
|
38
|
+
from pr_loop_shared_constants.claude_settings_keys_constants import (
|
|
46
39
|
CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
|
|
47
40
|
CLAUDE_SETTINGS_ALLOW_KEY,
|
|
48
41
|
CLAUDE_SETTINGS_AUTO_MODE_KEY,
|
|
42
|
+
CLAUDE_SETTINGS_DENY_KEY,
|
|
49
43
|
CLAUDE_SETTINGS_ENVIRONMENT_KEY,
|
|
50
44
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
51
45
|
)
|
|
@@ -76,6 +70,35 @@ def add_rules_to_allow_list(
|
|
|
76
70
|
)
|
|
77
71
|
|
|
78
72
|
|
|
73
|
+
def add_rules_to_deny_list(
|
|
74
|
+
all_settings: dict[str, object], all_rules_to_add: list[str]
|
|
75
|
+
) -> int:
|
|
76
|
+
"""Add permission rules to the settings deny list.
|
|
77
|
+
|
|
78
|
+
Deny rules take precedence over allow rules in Claude Code's permission
|
|
79
|
+
matching, so writing agent-config paths into the deny list forces a
|
|
80
|
+
per-edit user approval even when a broader allow rule would cover them.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
all_settings: The parsed settings dictionary.
|
|
84
|
+
all_rules_to_add: Permission rule strings to append.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Number of rules actually added (new entries).
|
|
88
|
+
"""
|
|
89
|
+
permissions_section = ensure_dict_section(
|
|
90
|
+
all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
|
|
91
|
+
)
|
|
92
|
+
existing_deny_list = ensure_list_entry(
|
|
93
|
+
permissions_section, CLAUDE_SETTINGS_DENY_KEY
|
|
94
|
+
)
|
|
95
|
+
return sum(
|
|
96
|
+
1
|
|
97
|
+
for each_rule in all_rules_to_add
|
|
98
|
+
if append_if_missing(existing_deny_list, each_rule)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
79
102
|
def add_directory_to_additional_directories(
|
|
80
103
|
all_settings: dict[str, object], directory_path: str
|
|
81
104
|
) -> int:
|
|
@@ -122,11 +145,63 @@ def add_auto_mode_environment_entry(
|
|
|
122
145
|
return 0
|
|
123
146
|
|
|
124
147
|
|
|
148
|
+
def purge_stale_trust_entries(
|
|
149
|
+
all_settings: dict[str, object],
|
|
150
|
+
project_path: str,
|
|
151
|
+
prefix: str,
|
|
152
|
+
protected_entry: str | None = None,
|
|
153
|
+
) -> int:
|
|
154
|
+
"""Remove every prior trust entry for the project from autoMode.environment.
|
|
155
|
+
|
|
156
|
+
A trust entry is any string in autoMode.environment whose prefix matches
|
|
157
|
+
the trust-entry marker and that contains the project's .claude/** path.
|
|
158
|
+
Purging stale entries before adding the current template prevents
|
|
159
|
+
accumulation across template revisions. The optional protected_entry
|
|
160
|
+
survives the purge so an entry byte-identical to the one about to be
|
|
161
|
+
re-added is not removed and re-added on every invocation, preserving the
|
|
162
|
+
idempotency contract documented on grant_permissions_for_current_directory.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
all_settings: The parsed settings dictionary.
|
|
166
|
+
project_path: The POSIX-style project root path.
|
|
167
|
+
prefix: The literal prefix that marks a trust entry.
|
|
168
|
+
protected_entry: Optional entry text that, when byte-equal to a
|
|
169
|
+
candidate, prevents removal. Pass the freshly-formatted current
|
|
170
|
+
template entry from grant to preserve idempotency. Revoke passes
|
|
171
|
+
None so every matching entry is removed.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Number of stale entries removed.
|
|
175
|
+
"""
|
|
176
|
+
auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
|
|
177
|
+
if not isinstance(auto_mode_section, dict):
|
|
178
|
+
return 0
|
|
179
|
+
existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
|
|
180
|
+
if not isinstance(existing_environment, list):
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
def _should_purge_candidate(candidate_entry: object) -> bool:
|
|
184
|
+
if not is_trust_entry_for_project(candidate_entry, project_path, prefix):
|
|
185
|
+
return False
|
|
186
|
+
if protected_entry is not None and candidate_entry == protected_entry:
|
|
187
|
+
return False
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
return remove_matching_entries_from_list(
|
|
191
|
+
existing_environment,
|
|
192
|
+
_should_purge_candidate,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
125
196
|
def grant_permissions_for_current_directory() -> None:
|
|
126
197
|
"""Grant Edit/Write/Read permissions for the current project directory.
|
|
127
198
|
|
|
128
199
|
Reads the current project path, constructs permission rules from config
|
|
129
|
-
constants, and writes them to ~/.claude/settings.json atomically.
|
|
200
|
+
constants, and writes them to ~/.claude/settings.json atomically. Adds
|
|
201
|
+
deny rules for agent-config paths so edits to settings, hooks, commands,
|
|
202
|
+
agents, skills, mcp.json, and CLAUDE.md still require per-edit user
|
|
203
|
+
approval. Purges any prior trust entries for this project before writing
|
|
204
|
+
the current template to prevent accumulation across template revisions.
|
|
130
205
|
|
|
131
206
|
Raises:
|
|
132
207
|
SystemExit: When the current directory is not a valid project root.
|
|
@@ -146,19 +221,37 @@ def grant_permissions_for_current_directory() -> None:
|
|
|
146
221
|
all_permission_rules = build_permission_rules(
|
|
147
222
|
project_path, ALL_PERMISSION_ALLOW_TOOLS
|
|
148
223
|
)
|
|
224
|
+
all_agent_config_deny_rules = build_agent_config_deny_rules(
|
|
225
|
+
project_path,
|
|
226
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
227
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
228
|
+
)
|
|
149
229
|
environment_entry = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE.format(
|
|
150
230
|
project_path=project_path
|
|
151
231
|
)
|
|
152
232
|
settings = load_settings(claude_user_settings_path)
|
|
153
|
-
|
|
233
|
+
allow_rules_added_count = add_rules_to_allow_list(settings, all_permission_rules)
|
|
234
|
+
deny_rules_added_count = add_rules_to_deny_list(
|
|
235
|
+
settings, all_agent_config_deny_rules
|
|
236
|
+
)
|
|
154
237
|
directories_added_count = add_directory_to_additional_directories(
|
|
155
238
|
settings, project_path
|
|
156
239
|
)
|
|
240
|
+
stale_trust_entries_purged_count = purge_stale_trust_entries(
|
|
241
|
+
settings,
|
|
242
|
+
project_path,
|
|
243
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
|
|
244
|
+
protected_entry=environment_entry,
|
|
245
|
+
)
|
|
157
246
|
environment_entries_added_count = add_auto_mode_environment_entry(
|
|
158
247
|
settings, environment_entry
|
|
159
248
|
)
|
|
160
249
|
total_changes_count = (
|
|
161
|
-
|
|
250
|
+
allow_rules_added_count
|
|
251
|
+
+ deny_rules_added_count
|
|
252
|
+
+ directories_added_count
|
|
253
|
+
+ stale_trust_entries_purged_count
|
|
254
|
+
+ environment_entries_added_count
|
|
162
255
|
)
|
|
163
256
|
if total_changes_count == 0:
|
|
164
257
|
print(f"Project path: {project_path}")
|
|
@@ -168,8 +261,19 @@ def grant_permissions_for_current_directory() -> None:
|
|
|
168
261
|
save_settings(claude_user_settings_path, settings)
|
|
169
262
|
print(f"Project path: {project_path}")
|
|
170
263
|
print(f"Settings file: {claude_user_settings_path}")
|
|
171
|
-
print(
|
|
264
|
+
print(
|
|
265
|
+
f"Allow rules added: {allow_rules_added_count} of {len(all_permission_rules)}"
|
|
266
|
+
)
|
|
267
|
+
print(
|
|
268
|
+
f"Deny rules added: {deny_rules_added_count} of "
|
|
269
|
+
f"{len(all_agent_config_deny_rules)}"
|
|
270
|
+
)
|
|
172
271
|
print(f"Additional directories added: {directories_added_count}")
|
|
272
|
+
if stale_trust_entries_purged_count > 0:
|
|
273
|
+
print(
|
|
274
|
+
f"Stale auto-mode environment entries purged: "
|
|
275
|
+
f"{stale_trust_entries_purged_count}"
|
|
276
|
+
)
|
|
173
277
|
print(f"Auto-mode environment entries added: {environment_entries_added_count}")
|
|
174
278
|
|
|
175
279
|
|
|
@@ -32,11 +32,11 @@ import urllib.request
|
|
|
32
32
|
from pathlib import Path
|
|
33
33
|
from typing import NoReturn
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
if
|
|
37
|
-
sys.path.insert(0,
|
|
35
|
+
parent_directory = str(Path(__file__).resolve().parent)
|
|
36
|
+
if parent_directory not in sys.path:
|
|
37
|
+
sys.path.insert(0, parent_directory)
|
|
38
38
|
|
|
39
|
-
from
|
|
39
|
+
from pr_loop_shared_constants.post_audit_thread_constants import ( # noqa: E402
|
|
40
40
|
ALL_GH_API_COMMAND_PARTS,
|
|
41
41
|
ALL_GH_API_USER_COMMAND_PARTS,
|
|
42
42
|
ALL_GH_AUTH_STATUS_COMMAND_PARTS,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Constants shared by grant_project_claude_permissions and revoke_project_claude_permissions."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pr_loop_shared_constants.preflight_constants import GIT_DIRECTORY_NAME
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"ALL_AGENT_CONFIG_DENY_TOOLS",
|
|
9
|
+
"ALL_AGENT_CONFIG_PATH_PATTERNS",
|
|
10
|
+
"ALL_PERMISSION_ALLOW_TOOLS",
|
|
11
|
+
"ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS",
|
|
12
|
+
"AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX",
|
|
13
|
+
"AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE",
|
|
14
|
+
"CLAUDE_SETTINGS_DIRECTORY_NAME",
|
|
15
|
+
"CLAUDE_SETTINGS_FILENAME",
|
|
16
|
+
"GIT_DIRECTORY_NAME",
|
|
17
|
+
"TEXT_FILE_ENCODING",
|
|
18
|
+
"UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH",
|
|
19
|
+
"get_claude_user_settings_path",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ALL_PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read")
|
|
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
|
+
|
|
71
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE: str = (
|
|
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."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS: tuple[str, ...] = ('"', "'")
|
|
79
|
+
|
|
80
|
+
CLAUDE_SETTINGS_DIRECTORY_NAME: str = ".claude"
|
|
81
|
+
|
|
82
|
+
CLAUDE_SETTINGS_FILENAME: str = "settings.json"
|
|
83
|
+
|
|
84
|
+
TEXT_FILE_ENCODING: str = "utf-8"
|
|
85
|
+
|
|
86
|
+
UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH: int = 8
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_claude_user_settings_path() -> Path:
|
|
90
|
+
return Path.home() / CLAUDE_SETTINGS_DIRECTORY_NAME / CLAUDE_SETTINGS_FILENAME
|
|
@@ -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"
|
|
@@ -4,38 +4,20 @@ import subprocess
|
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
parent_directory = str(Path(__file__).resolve().parent)
|
|
8
|
+
sys.path[:] = [
|
|
9
|
+
each_existing_entry
|
|
10
|
+
for each_existing_entry in sys.path
|
|
11
|
+
if not (
|
|
12
|
+
os.path.exists(each_existing_entry)
|
|
13
|
+
and os.path.samefile(each_existing_entry, parent_directory)
|
|
14
|
+
)
|
|
15
|
+
]
|
|
16
|
+
if parent_directory not in sys.path:
|
|
17
|
+
sys.path.insert(0, parent_directory)
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
str(_script_directory_resolved),
|
|
15
|
-
str(_script_directory_absolute),
|
|
16
|
-
):
|
|
17
|
-
return True
|
|
18
|
-
try:
|
|
19
|
-
candidate_path = Path(each_path_entry)
|
|
20
|
-
except (OSError, ValueError):
|
|
21
|
-
return False
|
|
22
|
-
if candidate_path.exists():
|
|
23
|
-
try:
|
|
24
|
-
return os.path.samefile(candidate_path, _script_directory_resolved)
|
|
25
|
-
except OSError:
|
|
26
|
-
return False
|
|
27
|
-
return False
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for each_index in range(len(sys.path) - 1, -1, -1):
|
|
31
|
-
if _entry_points_at_preflight_script_directory(sys.path[each_index]):
|
|
32
|
-
sys.path.pop(each_index)
|
|
33
|
-
_preflight_scripts_path_entry = str(_script_directory_absolute)
|
|
34
|
-
if _preflight_scripts_path_entry not in sys.path:
|
|
35
|
-
sys.path.insert(0, _preflight_scripts_path_entry)
|
|
36
|
-
|
|
37
|
-
from config.fix_hookspath_constants import HOOKS_PATH_VERIFICATION_SUFFIX
|
|
38
|
-
from config.preflight_constants import (
|
|
19
|
+
from pr_loop_shared_constants.fix_hookspath_constants import HOOKS_PATH_VERIFICATION_SUFFIX # noqa: E402
|
|
20
|
+
from pr_loop_shared_constants.preflight_constants import (
|
|
39
21
|
ALL_GIT_CONFIG_GET_CORE_HOOKS_PATH_SUBCOMMAND,
|
|
40
22
|
ALL_GIT_DIFF_NAME_ONLY_SUBCOMMAND,
|
|
41
23
|
ALL_GIT_LS_FILES_TEST_DISCOVERY_SUBCOMMAND,
|
|
@@ -8,22 +8,8 @@ rules and disabled-token taxonomy live in exactly one place.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
|
-
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
for each_cached_module_name in [
|
|
15
|
-
each_module_key
|
|
16
|
-
for each_module_key in list(sys.modules)
|
|
17
|
-
if each_module_key == "config" or each_module_key.startswith("config.")
|
|
18
|
-
]:
|
|
19
|
-
sys.modules.pop(each_cached_module_name, None)
|
|
20
|
-
_shared_pr_loop_scripts_directory = str(Path(__file__).absolute().parent)
|
|
21
|
-
while _shared_pr_loop_scripts_directory in sys.path:
|
|
22
|
-
sys.path.remove(_shared_pr_loop_scripts_directory)
|
|
23
|
-
if _shared_pr_loop_scripts_directory not in sys.path:
|
|
24
|
-
sys.path.insert(0, _shared_pr_loop_scripts_directory)
|
|
25
|
-
|
|
26
|
-
from config.reviews_disabled_constants import (
|
|
11
|
+
|
|
12
|
+
from pr_loop_shared_constants.reviews_disabled_constants import (
|
|
27
13
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
|
|
28
14
|
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
|
|
29
15
|
CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR,
|
|
@@ -10,41 +10,34 @@ autoMode sections so repeated grant/revoke cycles leave no dead structure.
|
|
|
10
10
|
import sys
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
parent_directory = str(Path(__file__).
|
|
14
|
-
try:
|
|
15
|
-
sys.path.remove(parent_directory)
|
|
16
|
-
except ValueError:
|
|
17
|
-
pass
|
|
13
|
+
parent_directory = str(Path(__file__).resolve().parent)
|
|
18
14
|
if parent_directory not in sys.path:
|
|
19
15
|
sys.path.insert(0, parent_directory)
|
|
20
16
|
|
|
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)
|
|
29
|
-
|
|
30
17
|
from _claude_permissions_common import ( # noqa: E402
|
|
18
|
+
build_agent_config_deny_rules,
|
|
31
19
|
build_permission_rules,
|
|
32
20
|
exit_with_error,
|
|
33
21
|
get_current_project_path,
|
|
22
|
+
is_trust_entry_for_project,
|
|
34
23
|
is_valid_project_root,
|
|
35
24
|
load_settings,
|
|
36
25
|
prune_empty_list_then_empty_section,
|
|
26
|
+
remove_matching_entries_from_list,
|
|
37
27
|
save_settings,
|
|
38
28
|
)
|
|
39
|
-
from
|
|
29
|
+
from pr_loop_shared_constants.claude_permissions_constants import (
|
|
30
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
31
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
40
32
|
ALL_PERMISSION_ALLOW_TOOLS,
|
|
41
|
-
|
|
33
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX,
|
|
42
34
|
get_claude_user_settings_path,
|
|
43
35
|
)
|
|
44
|
-
from
|
|
36
|
+
from pr_loop_shared_constants.claude_settings_keys_constants import (
|
|
45
37
|
CLAUDE_SETTINGS_ADDITIONAL_DIRECTORIES_KEY,
|
|
46
38
|
CLAUDE_SETTINGS_ALLOW_KEY,
|
|
47
39
|
CLAUDE_SETTINGS_AUTO_MODE_KEY,
|
|
40
|
+
CLAUDE_SETTINGS_DENY_KEY,
|
|
48
41
|
CLAUDE_SETTINGS_ENVIRONMENT_KEY,
|
|
49
42
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
50
43
|
)
|
|
@@ -92,6 +85,27 @@ def remove_rules_from_allow_list(
|
|
|
92
85
|
return remove_values_from_list(existing_allow_list, set(all_rules_to_remove))
|
|
93
86
|
|
|
94
87
|
|
|
88
|
+
def remove_rules_from_deny_list(
|
|
89
|
+
all_settings: dict[str, object], all_rules_to_remove: list[str]
|
|
90
|
+
) -> int:
|
|
91
|
+
"""Remove matching permission rules from the settings deny list.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
all_settings: The parsed settings dictionary.
|
|
95
|
+
all_rules_to_remove: Permission rule strings to remove.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Number of rules removed.
|
|
99
|
+
"""
|
|
100
|
+
permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
|
|
101
|
+
if not isinstance(permissions_section, dict):
|
|
102
|
+
return 0
|
|
103
|
+
existing_deny_list = permissions_section.get(CLAUDE_SETTINGS_DENY_KEY)
|
|
104
|
+
if not isinstance(existing_deny_list, list):
|
|
105
|
+
return 0
|
|
106
|
+
return remove_values_from_list(existing_deny_list, set(all_rules_to_remove))
|
|
107
|
+
|
|
108
|
+
|
|
95
109
|
def remove_directory_from_additional_directories(
|
|
96
110
|
all_settings: dict[str, object], directory_path: str
|
|
97
111
|
) -> int:
|
|
@@ -115,17 +129,23 @@ def remove_directory_from_additional_directories(
|
|
|
115
129
|
return remove_values_from_list(existing_directories, {directory_path})
|
|
116
130
|
|
|
117
131
|
|
|
118
|
-
def
|
|
119
|
-
all_settings: dict[str, object],
|
|
132
|
+
def remove_trust_entries_for_project(
|
|
133
|
+
all_settings: dict[str, object], project_path: str, prefix: str
|
|
120
134
|
) -> int:
|
|
121
|
-
"""Remove
|
|
135
|
+
"""Remove every trust entry for the project from autoMode.environment.
|
|
136
|
+
|
|
137
|
+
Matches any string in autoMode.environment whose prefix matches the
|
|
138
|
+
trust-entry marker and that contains the project's .claude/** path.
|
|
139
|
+
The match is wording-agnostic so prior template revisions are removed
|
|
140
|
+
cleanly even when the current template differs.
|
|
122
141
|
|
|
123
142
|
Args:
|
|
124
143
|
all_settings: The parsed settings dictionary.
|
|
125
|
-
|
|
144
|
+
project_path: The POSIX-style project root path.
|
|
145
|
+
prefix: The literal prefix that marks a trust entry.
|
|
126
146
|
|
|
127
147
|
Returns:
|
|
128
|
-
|
|
148
|
+
Number of entries removed.
|
|
129
149
|
"""
|
|
130
150
|
auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
|
|
131
151
|
if not isinstance(auto_mode_section, dict):
|
|
@@ -133,7 +153,12 @@ def remove_auto_mode_environment_entry(
|
|
|
133
153
|
existing_environment = auto_mode_section.get(CLAUDE_SETTINGS_ENVIRONMENT_KEY)
|
|
134
154
|
if not isinstance(existing_environment, list):
|
|
135
155
|
return 0
|
|
136
|
-
return
|
|
156
|
+
return remove_matching_entries_from_list(
|
|
157
|
+
existing_environment,
|
|
158
|
+
lambda candidate_entry: is_trust_entry_for_project(
|
|
159
|
+
candidate_entry, project_path, prefix
|
|
160
|
+
),
|
|
161
|
+
)
|
|
137
162
|
|
|
138
163
|
|
|
139
164
|
def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
@@ -147,6 +172,11 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
|
147
172
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
148
173
|
CLAUDE_SETTINGS_ALLOW_KEY,
|
|
149
174
|
)
|
|
175
|
+
prune_empty_list_then_empty_section(
|
|
176
|
+
all_settings,
|
|
177
|
+
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
178
|
+
CLAUDE_SETTINGS_DENY_KEY,
|
|
179
|
+
)
|
|
150
180
|
prune_empty_list_then_empty_section(
|
|
151
181
|
all_settings,
|
|
152
182
|
CLAUDE_SETTINGS_PERMISSIONS_KEY,
|
|
@@ -162,9 +192,10 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
|
|
|
162
192
|
def revoke_permissions_for_current_directory() -> None:
|
|
163
193
|
"""Revoke permissions previously granted for the current project directory.
|
|
164
194
|
|
|
165
|
-
Reads the current project path, constructs the matching
|
|
166
|
-
removes them from ~/.claude/settings.json,
|
|
167
|
-
|
|
195
|
+
Reads the current project path, constructs the matching allow and deny
|
|
196
|
+
permission rules, removes them from ~/.claude/settings.json, removes
|
|
197
|
+
every trust entry for the project from autoMode.environment, and prunes
|
|
198
|
+
any newly empty sections.
|
|
168
199
|
|
|
169
200
|
Raises:
|
|
170
201
|
SystemExit: When the current directory is not a valid project root.
|
|
@@ -182,19 +213,25 @@ def revoke_permissions_for_current_directory() -> None:
|
|
|
182
213
|
raise SystemExit(1)
|
|
183
214
|
project_path = get_current_project_path()
|
|
184
215
|
permission_rules = build_permission_rules(project_path, ALL_PERMISSION_ALLOW_TOOLS)
|
|
185
|
-
|
|
186
|
-
project_path
|
|
216
|
+
all_agent_config_deny_rules = build_agent_config_deny_rules(
|
|
217
|
+
project_path,
|
|
218
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
219
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
187
220
|
)
|
|
188
221
|
settings = load_settings(claude_user_settings_path)
|
|
189
|
-
|
|
222
|
+
allow_rules_removed_count = remove_rules_from_allow_list(settings, permission_rules)
|
|
223
|
+
deny_rules_removed_count = remove_rules_from_deny_list(
|
|
224
|
+
settings, all_agent_config_deny_rules
|
|
225
|
+
)
|
|
190
226
|
directories_removed_count = remove_directory_from_additional_directories(
|
|
191
227
|
settings, project_path
|
|
192
228
|
)
|
|
193
|
-
environment_entries_removed_count =
|
|
194
|
-
settings,
|
|
229
|
+
environment_entries_removed_count = remove_trust_entries_for_project(
|
|
230
|
+
settings, project_path, AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX
|
|
195
231
|
)
|
|
196
232
|
total_changes_count = (
|
|
197
|
-
|
|
233
|
+
allow_rules_removed_count
|
|
234
|
+
+ deny_rules_removed_count
|
|
198
235
|
+ directories_removed_count
|
|
199
236
|
+ environment_entries_removed_count
|
|
200
237
|
)
|
|
@@ -207,7 +244,13 @@ def revoke_permissions_for_current_directory() -> None:
|
|
|
207
244
|
save_settings(claude_user_settings_path, settings)
|
|
208
245
|
print(f"Project path: {project_path}")
|
|
209
246
|
print(f"Settings file: {claude_user_settings_path}")
|
|
210
|
-
print(
|
|
247
|
+
print(
|
|
248
|
+
f"Allow rules removed: {allow_rules_removed_count} of {len(permission_rules)}"
|
|
249
|
+
)
|
|
250
|
+
print(
|
|
251
|
+
f"Deny rules removed: {deny_rules_removed_count} of "
|
|
252
|
+
f"{len(all_agent_config_deny_rules)}"
|
|
253
|
+
)
|
|
211
254
|
print(f"Additional directories removed: {directories_removed_count}")
|
|
212
255
|
print(
|
|
213
256
|
f"Auto-mode environment entries removed: {environment_entries_removed_count}"
|
|
@@ -1,51 +1 @@
|
|
|
1
|
-
"""Test fixtures for _shared/pr-loop/scripts/.
|
|
2
|
-
|
|
3
|
-
Two unrelated Python packages live under the name ``config`` in this repo:
|
|
4
|
-
- ``_shared/pr-loop/scripts/config/`` (constants for grant/revoke/gate/preflight scripts)
|
|
5
|
-
- ``hooks/config/`` (constants for the code-rules enforcer and other hooks)
|
|
6
|
-
|
|
7
|
-
When tests under this directory exercise the gate (which loads
|
|
8
|
-
``hooks/blocking/code_rules_enforcer.py``) and also load the grant/revoke
|
|
9
|
-
scripts in the same pytest process, ``sys.modules['config']`` and
|
|
10
|
-
``sys.modules['config.<submodule>']`` cache entries from one package leak
|
|
11
|
-
into the other. The next ``from config.<submodule> import ...`` then fails
|
|
12
|
-
with ``ModuleNotFoundError`` because the cached parent package does not
|
|
13
|
-
expose that submodule.
|
|
14
|
-
|
|
15
|
-
Independently, several scripts in this folder do
|
|
16
|
-
``Path(__file__).resolve()`` then prepend the resulting directory to
|
|
17
|
-
``sys.path``. On Windows when the working tree lives under a mapped drive
|
|
18
|
-
backed by a UNC share (``Y:`` -> ``\\\\server\\share\\...``), ``.resolve()``
|
|
19
|
-
returns the UNC form, and Python's import machinery on this host cannot
|
|
20
|
-
locate ``config`` packages from a UNC ``sys.path`` entry. The Y:-form entry
|
|
21
|
-
gets pushed to a later index by subsequent inserts, making ``from
|
|
22
|
-
config.<submodule> import ...`` fail.
|
|
23
|
-
|
|
24
|
-
This autouse fixture restores both invariants before each test:
|
|
25
|
-
1. evict every ``config`` and ``config.*`` entry from ``sys.modules``
|
|
26
|
-
2. prepend the drive-letter (``.absolute()``) form of the scripts
|
|
27
|
-
directory to ``sys.path`` so package resolution always has a
|
|
28
|
-
non-UNC path to search first
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
from __future__ import annotations
|
|
32
|
-
|
|
33
|
-
import sys
|
|
34
|
-
from pathlib import Path
|
|
35
|
-
|
|
36
|
-
import pytest
|
|
37
|
-
|
|
38
|
-
SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM = str(Path(__file__).absolute().parent.parent)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@pytest.fixture(autouse=True)
|
|
42
|
-
def _evict_config_namespace_between_tests() -> None:
|
|
43
|
-
for each_module_name in [
|
|
44
|
-
each_key
|
|
45
|
-
for each_key in list(sys.modules)
|
|
46
|
-
if each_key == "config" or each_key.startswith("config.")
|
|
47
|
-
]:
|
|
48
|
-
sys.modules.pop(each_module_name, None)
|
|
49
|
-
if SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM in sys.path:
|
|
50
|
-
sys.path.remove(SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM)
|
|
51
|
-
sys.path.insert(0, SCRIPTS_DIRECTORY_DRIVE_LETTER_FORM)
|
|
1
|
+
"""Test fixtures for _shared/pr-loop/scripts/tests/."""
|