claude-dev-env 2.4.0 → 2.5.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 +53 -49
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +2 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +0 -1
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -44
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/scripts/test_invoke_code_review.py +0 -966
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -8,16 +8,31 @@ pr_loop_shared_constants/claude_settings_keys_constants.py.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import importlib.util
|
|
11
|
+
import json
|
|
11
12
|
import sys
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from types import ModuleType
|
|
14
15
|
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
15
18
|
|
|
16
19
|
def _load_revoke_module() -> ModuleType:
|
|
17
20
|
scripts_directory = Path(__file__).parent.parent
|
|
18
21
|
parent_directory = str(scripts_directory.resolve())
|
|
19
|
-
if parent_directory
|
|
20
|
-
sys.path.
|
|
22
|
+
if parent_directory in sys.path:
|
|
23
|
+
sys.path.remove(parent_directory)
|
|
24
|
+
sys.path.insert(0, parent_directory)
|
|
25
|
+
for each_module_name in list(sys.modules):
|
|
26
|
+
if each_module_name == "pr_loop_shared_constants" or each_module_name.startswith(
|
|
27
|
+
"pr_loop_shared_constants."
|
|
28
|
+
):
|
|
29
|
+
del sys.modules[each_module_name]
|
|
30
|
+
if each_module_name in {
|
|
31
|
+
"_claude_permissions_common",
|
|
32
|
+
"revoke_project_claude_permissions",
|
|
33
|
+
"stale_worktree_rule_sweep",
|
|
34
|
+
}:
|
|
35
|
+
del sys.modules[each_module_name]
|
|
21
36
|
module_path = scripts_directory / "revoke_project_claude_permissions.py"
|
|
22
37
|
specification = importlib.util.spec_from_file_location(
|
|
23
38
|
"revoke_project_claude_permissions", module_path
|
|
@@ -29,6 +44,172 @@ def _load_revoke_module() -> ModuleType:
|
|
|
29
44
|
return module
|
|
30
45
|
|
|
31
46
|
|
|
47
|
+
def _write_legacy_form_settings_file(
|
|
48
|
+
settings_path: Path, revoke_module: ModuleType, project_path: str
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Seed a settings file with only pre-#158 legacy Write()/Glob() rules.
|
|
51
|
+
|
|
52
|
+
One Write(/.claude/**) allow rule, and a Write(/.claude/<pattern>) plus
|
|
53
|
+
a Glob(/.claude/<pattern>) deny rule for every agent-config path
|
|
54
|
+
pattern -- the exact stranded-rule shapes issue #233 describes."""
|
|
55
|
+
legacy_allow_rule = f"Write({project_path}/.claude/**)"
|
|
56
|
+
legacy_deny_rules = [
|
|
57
|
+
f"Write({project_path}/.claude/{each_pattern})"
|
|
58
|
+
for each_pattern in revoke_module.ALL_AGENT_CONFIG_PATH_PATTERNS
|
|
59
|
+
] + [
|
|
60
|
+
f"Glob({project_path}/.claude/{each_pattern})"
|
|
61
|
+
for each_pattern in revoke_module.ALL_AGENT_CONFIG_PATH_PATTERNS
|
|
62
|
+
]
|
|
63
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
settings_path.write_text(
|
|
65
|
+
json.dumps(
|
|
66
|
+
{
|
|
67
|
+
"permissions": {
|
|
68
|
+
"allow": [legacy_allow_rule],
|
|
69
|
+
"deny": legacy_deny_rules,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
encoding="utf-8",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_revoke_reaps_legacy_write_and_glob_rules_leaving_no_dead_structure(
|
|
78
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
79
|
+
) -> None:
|
|
80
|
+
"""A revoke run reaps the legacy Write()/Glob() rules an older grant left.
|
|
81
|
+
|
|
82
|
+
No current-form Edit/Read rule is present, so a revoke run that reaps
|
|
83
|
+
only the mint set would remove nothing and leave every legacy rule
|
|
84
|
+
stranded. After revoke runs, the permissions section is gone entirely,
|
|
85
|
+
matching the module's "no dead structure" contract."""
|
|
86
|
+
revoke_module = _load_revoke_module()
|
|
87
|
+
project_directory = tmp_path / "project"
|
|
88
|
+
project_directory.mkdir()
|
|
89
|
+
(project_directory / ".git").mkdir()
|
|
90
|
+
settings_path = tmp_path / "home" / ".claude" / "settings.json"
|
|
91
|
+
project_path = str(project_directory).replace("\\", "/")
|
|
92
|
+
_write_legacy_form_settings_file(settings_path, revoke_module, project_path)
|
|
93
|
+
|
|
94
|
+
monkeypatch.setattr(
|
|
95
|
+
revoke_module, "get_claude_user_settings_path", lambda: settings_path
|
|
96
|
+
)
|
|
97
|
+
monkeypatch.chdir(project_directory)
|
|
98
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
99
|
+
|
|
100
|
+
saved_settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
101
|
+
assert "permissions" not in saved_settings
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_revoke_strips_inert_write_glob_notebookedit_on_foreign_worktree_paths(
|
|
105
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Global inert strip removes Write/Glob/NotebookEdit for any path.
|
|
108
|
+
|
|
109
|
+
Foreign worktree rules are outside the cwd project's exact path, so
|
|
110
|
+
project-scoped #315 reap alone leaves them; revoke must strip by tool."""
|
|
111
|
+
revoke_module = _load_revoke_module()
|
|
112
|
+
project_directory = tmp_path / "operator_project"
|
|
113
|
+
project_directory.mkdir()
|
|
114
|
+
(project_directory / ".git").mkdir()
|
|
115
|
+
settings_path = tmp_path / "home" / ".claude" / "settings.json"
|
|
116
|
+
foreign_worktree = (
|
|
117
|
+
"c:/Users/jon/.claude/worktrees/claude-dev-env-702a0c2055d2/other-wt"
|
|
118
|
+
)
|
|
119
|
+
live_other_project = "C:/dev/live-other-project"
|
|
120
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
settings_path.write_text(
|
|
122
|
+
json.dumps(
|
|
123
|
+
{
|
|
124
|
+
"permissions": {
|
|
125
|
+
"allow": [
|
|
126
|
+
f"Write({foreign_worktree}/**)",
|
|
127
|
+
f"NotebookEdit(**/.claude/worktrees/**)",
|
|
128
|
+
f"Edit({live_other_project}/.claude/**)",
|
|
129
|
+
f"Read({live_other_project}/.claude/**)",
|
|
130
|
+
"Bash(echo hi)",
|
|
131
|
+
],
|
|
132
|
+
"deny": [
|
|
133
|
+
f"Glob({foreign_worktree}/.claude/hooks/**)",
|
|
134
|
+
f"Write({foreign_worktree}/.claude/skills/**)",
|
|
135
|
+
f"Edit({live_other_project}/.claude/hooks/**)",
|
|
136
|
+
],
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
),
|
|
140
|
+
encoding="utf-8",
|
|
141
|
+
)
|
|
142
|
+
monkeypatch.setattr(
|
|
143
|
+
revoke_module, "get_claude_user_settings_path", lambda: settings_path
|
|
144
|
+
)
|
|
145
|
+
monkeypatch.chdir(project_directory)
|
|
146
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
147
|
+
|
|
148
|
+
saved_settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
149
|
+
permissions_section = saved_settings["permissions"]
|
|
150
|
+
allow_rules = permissions_section["allow"]
|
|
151
|
+
deny_rules = permissions_section["deny"]
|
|
152
|
+
assert allow_rules == [
|
|
153
|
+
f"Edit({live_other_project}/.claude/**)",
|
|
154
|
+
f"Read({live_other_project}/.claude/**)",
|
|
155
|
+
"Bash(echo hi)",
|
|
156
|
+
]
|
|
157
|
+
assert deny_rules == [f"Edit({live_other_project}/.claude/hooks/**)"]
|
|
158
|
+
for each_rule in allow_rules + deny_rules:
|
|
159
|
+
tool_name = each_rule.split("(", 1)[0]
|
|
160
|
+
assert tool_name not in ("Write", "Glob", "NotebookEdit")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_revoke_from_home_reaps_dollar_home_grant_shapes(
|
|
164
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
165
|
+
) -> None:
|
|
166
|
+
"""When cwd is the user home, Edit($HOME/.claude/**) shapes are reaped."""
|
|
167
|
+
revoke_module = _load_revoke_module()
|
|
168
|
+
home_directory = tmp_path / "fake_home"
|
|
169
|
+
home_directory.mkdir()
|
|
170
|
+
(home_directory / ".git").mkdir()
|
|
171
|
+
(home_directory / ".claude").mkdir()
|
|
172
|
+
settings_path = tmp_path / "settings_home" / ".claude" / "settings.json"
|
|
173
|
+
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
live_other_project = "C:/dev/unrelated-live"
|
|
175
|
+
settings_path.write_text(
|
|
176
|
+
json.dumps(
|
|
177
|
+
{
|
|
178
|
+
"permissions": {
|
|
179
|
+
"allow": [
|
|
180
|
+
"Edit($HOME/.claude/**)",
|
|
181
|
+
"Write($HOME/.claude/**)",
|
|
182
|
+
"Read($HOME/.claude/**)",
|
|
183
|
+
f"Edit({live_other_project}/.claude/**)",
|
|
184
|
+
],
|
|
185
|
+
"deny": [
|
|
186
|
+
"Edit($HOME/.claude/hooks/**)",
|
|
187
|
+
"Glob($HOME/.claude/skills/**)",
|
|
188
|
+
f"Read({live_other_project}/.claude/hooks/**)",
|
|
189
|
+
],
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
),
|
|
193
|
+
encoding="utf-8",
|
|
194
|
+
)
|
|
195
|
+
monkeypatch.setattr(
|
|
196
|
+
revoke_module, "get_claude_user_settings_path", lambda: settings_path
|
|
197
|
+
)
|
|
198
|
+
monkeypatch.setattr(
|
|
199
|
+
Path, "home", classmethod(lambda _cls: home_directory), raising=True
|
|
200
|
+
)
|
|
201
|
+
monkeypatch.chdir(home_directory)
|
|
202
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
203
|
+
|
|
204
|
+
saved_settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
205
|
+
permissions_section = saved_settings["permissions"]
|
|
206
|
+
allow_rules = permissions_section["allow"]
|
|
207
|
+
deny_rules = permissions_section["deny"]
|
|
208
|
+
assert allow_rules == [f"Edit({live_other_project}/.claude/**)"]
|
|
209
|
+
assert deny_rules == [f"Read({live_other_project}/.claude/hooks/**)"]
|
|
210
|
+
assert not any("$HOME" in each_rule for each_rule in allow_rules + deny_rules)
|
|
211
|
+
|
|
212
|
+
|
|
32
213
|
def test_module_imports_constants_from_config_modules() -> None:
|
|
33
214
|
revoke_module = _load_revoke_module()
|
|
34
215
|
assert revoke_module.ALL_PERMISSION_ALLOW_TOOLS == ("Edit", "Read")
|
|
@@ -38,6 +219,134 @@ def test_module_imports_constants_from_config_modules() -> None:
|
|
|
38
219
|
assert revoke_module.CLAUDE_SETTINGS_PERMISSIONS_KEY == "permissions"
|
|
39
220
|
|
|
40
221
|
|
|
222
|
+
def test_remove_inert_file_permission_rules_strips_only_inert_tools() -> None:
|
|
223
|
+
revoke_module = _load_revoke_module()
|
|
224
|
+
settings: dict[str, object] = {
|
|
225
|
+
"permissions": {
|
|
226
|
+
"allow": [
|
|
227
|
+
"Write(/foreign/.claude/**)",
|
|
228
|
+
"NotebookEdit(**/.claude/worktrees/**)",
|
|
229
|
+
"Edit(/live/.claude/**)",
|
|
230
|
+
],
|
|
231
|
+
"deny": ["Glob(/foreign/.claude/hooks/**)", "Read(/live/.claude/hooks/**)"],
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
removed_count = revoke_module.remove_inert_file_permission_rules(settings)
|
|
235
|
+
assert removed_count == 3
|
|
236
|
+
permissions_section = _section_of(settings, "permissions")
|
|
237
|
+
assert permissions_section["allow"] == ["Edit(/live/.claude/**)"]
|
|
238
|
+
assert permissions_section["deny"] == ["Read(/live/.claude/hooks/**)"]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_strip_inert_rules_from_settings_file_missing_path_is_noop(
|
|
242
|
+
tmp_path: Path,
|
|
243
|
+
) -> None:
|
|
244
|
+
revoke_module = _load_revoke_module()
|
|
245
|
+
missing_path = revoke_module.get_claude_project_local_settings_path(tmp_path)
|
|
246
|
+
assert not missing_path.is_file()
|
|
247
|
+
assert revoke_module.strip_inert_rules_from_settings_file(missing_path) == 0
|
|
248
|
+
assert not missing_path.is_file()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_strip_inert_rules_from_settings_file_updates_project_local_file(
|
|
252
|
+
tmp_path: Path,
|
|
253
|
+
) -> None:
|
|
254
|
+
revoke_module = _load_revoke_module()
|
|
255
|
+
local_settings_path = revoke_module.get_claude_project_local_settings_path(tmp_path)
|
|
256
|
+
local_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
local_settings_path.write_text(
|
|
258
|
+
json.dumps(
|
|
259
|
+
{
|
|
260
|
+
"permissions": {
|
|
261
|
+
"allow": [
|
|
262
|
+
"Write(//**/.claude/**)",
|
|
263
|
+
r"Write(C:\\**\\.claude\\**)",
|
|
264
|
+
"Edit(C:/dev/live/.claude/**)",
|
|
265
|
+
"Bash(echo hi)",
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
encoding="utf-8",
|
|
271
|
+
)
|
|
272
|
+
removed_count = revoke_module.strip_inert_rules_from_settings_file(
|
|
273
|
+
local_settings_path
|
|
274
|
+
)
|
|
275
|
+
assert removed_count == 2
|
|
276
|
+
saved_settings = json.loads(local_settings_path.read_text(encoding="utf-8"))
|
|
277
|
+
allow_rules = saved_settings["permissions"]["allow"]
|
|
278
|
+
assert allow_rules == ["Edit(C:/dev/live/.claude/**)", "Bash(echo hi)"]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_revoke_strips_inert_rules_from_project_settings_local_json(
|
|
282
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
283
|
+
) -> None:
|
|
284
|
+
"""User settings may already be clean; project settings.local still warns.
|
|
285
|
+
|
|
286
|
+
Claude loads .claude/settings.local.json for the cwd project; revoke must
|
|
287
|
+
strip inert Write/Glob/NotebookEdit there too."""
|
|
288
|
+
revoke_module = _load_revoke_module()
|
|
289
|
+
project_directory = tmp_path / "project"
|
|
290
|
+
project_directory.mkdir()
|
|
291
|
+
(project_directory / ".git").mkdir()
|
|
292
|
+
user_settings_path = tmp_path / "home" / ".claude" / "settings.json"
|
|
293
|
+
user_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
user_settings_path.write_text(json.dumps({"permissions": {"allow": []}}), encoding="utf-8")
|
|
295
|
+
local_settings_path = revoke_module.get_claude_project_local_settings_path(
|
|
296
|
+
project_directory
|
|
297
|
+
)
|
|
298
|
+
local_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
299
|
+
local_settings_path.write_text(
|
|
300
|
+
json.dumps(
|
|
301
|
+
{
|
|
302
|
+
"permissions": {
|
|
303
|
+
"allow": [
|
|
304
|
+
"Write(//**/.claude/**)",
|
|
305
|
+
r"Write(C:\\**\\.claude\\**)",
|
|
306
|
+
"Read(C:/other/.claude/**)",
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
),
|
|
311
|
+
encoding="utf-8",
|
|
312
|
+
)
|
|
313
|
+
monkeypatch.setattr(
|
|
314
|
+
revoke_module, "get_claude_user_settings_path", lambda: user_settings_path
|
|
315
|
+
)
|
|
316
|
+
monkeypatch.chdir(project_directory)
|
|
317
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
318
|
+
|
|
319
|
+
local_settings = json.loads(local_settings_path.read_text(encoding="utf-8"))
|
|
320
|
+
allow_rules = local_settings["permissions"]["allow"]
|
|
321
|
+
assert allow_rules == ["Read(C:/other/.claude/**)"]
|
|
322
|
+
assert not any(
|
|
323
|
+
each_rule.split("(", 1)[0] in ("Write", "Glob", "NotebookEdit")
|
|
324
|
+
for each_rule in allow_rules
|
|
325
|
+
if isinstance(each_rule, str) and "(" in each_rule
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_build_project_scoped_allow_rules_for_reap_includes_legacy_tools() -> None:
|
|
330
|
+
revoke_module = _load_revoke_module()
|
|
331
|
+
all_allow_rules = revoke_module.build_project_scoped_allow_rules_for_reap(
|
|
332
|
+
"/repo/project"
|
|
333
|
+
)
|
|
334
|
+
assert "Edit(/repo/project/.claude/**)" in all_allow_rules
|
|
335
|
+
assert "Write(/repo/project/.claude/**)" in all_allow_rules
|
|
336
|
+
assert "Read(/repo/project/.claude/**)" in all_allow_rules
|
|
337
|
+
assert "Glob(/repo/project/.claude/**)" in all_allow_rules
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_build_project_scoped_deny_rules_for_reap_includes_legacy_tools() -> None:
|
|
341
|
+
revoke_module = _load_revoke_module()
|
|
342
|
+
all_deny_rules = revoke_module.build_project_scoped_deny_rules_for_reap(
|
|
343
|
+
"/repo/project"
|
|
344
|
+
)
|
|
345
|
+
assert "Edit(/repo/project/.claude/hooks/**)" in all_deny_rules
|
|
346
|
+
assert "Write(/repo/project/.claude/hooks/**)" in all_deny_rules
|
|
347
|
+
assert "Glob(/repo/project/.claude/skills/**)" in all_deny_rules
|
|
348
|
+
|
|
349
|
+
|
|
41
350
|
def test_revoke_module_guards_sys_path_insert_against_duplicates() -> None:
|
|
42
351
|
"""revoke_project_claude_permissions.py must guard its sys.path.insert with a
|
|
43
352
|
membership check so re-imports under test harnesses do not push duplicate
|