claude-dev-env 1.35.0 → 1.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/bin/install.mjs +28 -8
  3. package/bin/install.test.mjs +9 -1
  4. package/docs/CODE_RULES.md +3 -0
  5. package/docs/agents-md-alignment-plan.md +123 -0
  6. package/hooks/blocking/code_rules_enforcer.py +451 -39
  7. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  8. package/hooks/blocking/test_code_rules_enforcer.py +182 -0
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  10. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  11. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
  12. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
  13. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  14. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
  15. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
  16. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  17. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  18. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  19. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  20. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  21. package/hooks/config/banned_identifiers_constants.py +24 -0
  22. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  23. package/hooks/config/hook_log_extractor_constants.py +1 -1
  24. package/hooks/config/pre_tool_use_stdin.py +48 -0
  25. package/hooks/config/setup_project_paths_constants.py +4 -0
  26. package/hooks/config/stuttering_check_config.py +14 -0
  27. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  28. package/hooks/config/sys_path_insert_constants.py +4 -0
  29. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  30. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  31. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  32. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  33. package/hooks/config/unused_module_import_constants.py +7 -0
  34. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  35. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  36. package/hooks/git-hooks/config.py +3 -3
  37. package/hooks/git-hooks/test_gate_utils.py +10 -10
  38. package/hooks/mypy.ini +2 -0
  39. package/package.json +1 -1
  40. package/rules/gh-paginate.md +125 -0
  41. package/skills/bugteam/CONSTRAINTS.md +12 -6
  42. package/skills/bugteam/SKILL.md +364 -154
  43. package/skills/bugteam/SKILL_EVALS.md +25 -23
  44. package/skills/bugteam/reference/README.md +2 -0
  45. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  46. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  47. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  48. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  49. package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
  50. package/skills/bugteam/test_skill_additions.py +13 -4
  51. package/skills/bugteam/test_team_lifecycle.py +103 -0
  52. package/skills/findbugs/SKILL.md +3 -3
  53. package/skills/fixbugs/SKILL.md +4 -4
  54. package/skills/monitor-open-prs/SKILL.md +32 -2
  55. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  56. package/skills/pr-converge/SKILL.md +1206 -131
  57. package/skills/pr-converge/scripts/README.md +145 -0
  58. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  59. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  60. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  61. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  62. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  63. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  64. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  65. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  66. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  67. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  68. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  69. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  70. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  71. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  72. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  73. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  74. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  75. package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
  76. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  77. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  78. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  79. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  80. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  81. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  82. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  83. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  84. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  85. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  86. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  87. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  88. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  89. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  90. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  91. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  92. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  93. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  94. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  95. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  96. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  97. package/skills/pr-converge/test_team_lifecycle.py +56 -0
  98. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  99. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  100. package/skills/qbug/SKILL.md +4 -4
  101. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  102. package/skills/resume-review/SKILL.md +261 -0
  103. package/skills/bugteam/scripts/README.md +0 -58
  104. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  105. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  106. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  107. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  108. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  109. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  110. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  111. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  112. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  113. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  114. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  115. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -0,0 +1,261 @@
1
+ ---
2
+ name: resume-review
3
+ description: >-
4
+ Audit a resume for nitty-gritty offenses that bury signal. Resumes are
5
+ overviews scanned in 6-30 seconds, not detailed proof artifacts. This skill
6
+ identifies bullets that drift into workflow internals, multi-item technical
7
+ lists, library names, mechanism narration, or implementation detail, and
8
+ proposes tighter rewrites that keep the proof points (numbers, scope,
9
+ outcomes) while dropping the granular how-it-works text. Triggers:
10
+ "/resume-review", "review my resume", "audit this resume", "is my resume too
11
+ detailed", "tighten my resume bullets".
12
+ ---
13
+
14
+ # Resume Review
15
+
16
+ A resume is an **overview**. Hiring managers scan a resume in 6-30 seconds on
17
+ first pass. Granular technical detail buries the signal. Stats, scope, and
18
+ quantified outcomes belong on the resume; workflow internals, multi-item
19
+ technical lists, and how-it-works narration belong in the interview.
20
+
21
+ ## Source authority
22
+
23
+ Three established sources govern this skill. When a bullet violates one of
24
+ these principles, cite the source in the review comment.
25
+
26
+ - **Laszlo Bock**, *Work Rules!* (2015), Twelve Books, p. 71 (Bock served as
27
+ SVP People Operations at Google, 2006-2016): bullets describe "what you
28
+ accomplished, not what you did." Quantified outcomes beat task descriptions.
29
+ - **Steve Dalton**, *The 2-Hour Job Search* (2012), Ten Speed Press, Ch. 3:
30
+ hiring managers initially scan a resume in 6-30 seconds. Granular technical
31
+ detail buries the signal in that scan.
32
+ - **Lou Adler**, *Hire With Your Head* (4th ed., 2021), Wiley: bullets convey
33
+ **scope and impact**, not tools and tasks.
34
+
35
+ ## The principle
36
+
37
+ Every bullet earns its place by carrying one of three things:
38
+
39
+ 1. **Scope** — what surface area of work this covered (catalog size, team
40
+ size, user count, transaction volume, geographic reach).
41
+ 2. **Impact** — the outcome the work produced (revenue, time saved, error
42
+ rate reduced, problem prevented, capability unlocked).
43
+ 3. **Quantified proof** — a single number that anchors scope or impact and
44
+ makes the bullet harder to dismiss as boilerplate.
45
+
46
+ Bullets that describe the **mechanism** of how the work was done (workflow
47
+ steps, internal architecture, library choices, sequencing of operations) fail
48
+ the overview test. Move that content to the interview.
49
+
50
+ ## Offense categories
51
+
52
+ The audit walks every bullet and flags any of these patterns. Each pattern
53
+ has a fix template.
54
+
55
+ ### 1. Multi-item parenthetical lists
56
+
57
+ A parenthetical that enumerates 3+ named items (tools, systems, features,
58
+ sub-tasks).
59
+
60
+ > **Offender:** "Built and maintain three production Python automation
61
+ > systems (theme submission, theme exports, certification failure
62
+ > processing) backed by a shared utility library."
63
+
64
+ > **Fix:** Drop the parenthetical. Keep the count. "Built and maintain three
65
+ > production Python automations handling [scope]."
66
+
67
+ ### 2. Workflow-step narration
68
+
69
+ The bullet describes the sequence of operations the work performs.
70
+
71
+ > **Offender:** "Manage Samsung's annual catalog-update cycle when 2,800+
72
+ > themes must each be updated, exported, and resubmitted one by one within a
73
+ > 1-2 month window."
74
+
75
+ > **Fix:** Drop the workflow steps. Keep the scope numbers. "Manage Samsung's
76
+ > annual catalog-update cycle: 2,800+ themes refreshed within a 1-2 month
77
+ > window."
78
+
79
+ ### 3. Engineering-pattern jargon for non-engineering audience
80
+
81
+ Multiple named technical patterns in one bullet, written for an audience
82
+ that does not share the domain (e.g., infrastructure terms in an operations
83
+ or trust-and-safety resume).
84
+
85
+ > **Offender:** "Turn recurring failures into stronger error classification
86
+ > (transient vs. permanent), circuit breakers, and checkpoint/resume
87
+ > recovery."
88
+
89
+ > **Fix:** Replace the pattern enumeration with a single capability noun.
90
+ > "Turn recurring failures into reusable error-handling patterns so the same
91
+ > failure does not happen twice."
92
+
93
+ ### 4. Mechanism narration
94
+
95
+ The bullet explains how a tool or system internally works.
96
+
97
+ > **Offender:** "Author and maintain pr-converge, a Claude Code skill that
98
+ > automates the pull request review-and-fix loop until reviewers converge on
99
+ > ready, with three open pull requests (11,000+ lines) actively extending
100
+ > it."
101
+
102
+ > **Fix:** State the outcome the skill produces. Drop the loop description.
103
+ > "Author and maintain pr-converge, an open-source Claude Code skill that
104
+ > drives draft pull requests to merge-ready autonomously."
105
+
106
+ ### 5. Library or tool names embedded mid-bullet
107
+
108
+ A library, framework, or tool name appears inside a sentence about
109
+ capability. Library names belong in the dedicated Tools section, not in
110
+ capability bullets.
111
+
112
+ > **Offender:** "Implemented authenticated workflows end-to-end: account
113
+ > creation, token handling, local-first data with IndexedDB/Dexie, and
114
+ > conflict-aware sync between client and server."
115
+
116
+ > **Fix:** Drop the library names. State the capabilities. "Implemented
117
+ > authentication, local-first data persistence, and conflict-aware
118
+ > client-server sync end-to-end."
119
+
120
+ ### 6. Redundant statistics
121
+
122
+ Two numbers in the same bullet that express the same quantity at different
123
+ granularities.
124
+
125
+ > **Offender:** "Roughly 30 new theme submissions per day (150 per week)."
126
+
127
+ > **Fix:** Pick one. The daily number is usually the strongest scan signal.
128
+
129
+ ### 7. Implementation detail
130
+
131
+ Phrases that describe how the work was built rather than what it does.
132
+
133
+ > **Offender:** "Backed by a shared utility library, with zero manual
134
+ > intervention beyond starting the script."
135
+
136
+ > **Fix:** Drop the implementation phrase entirely. The capability statement
137
+ > should stand on its own.
138
+
139
+ ### 8. Two-sentence bullets
140
+
141
+ A bullet that requires two sentences usually carries one bullet of scope and
142
+ one bullet of mechanism. The mechanism sentence should be removed, not
143
+ combined.
144
+
145
+ > **Offender:** "Author and maintain pr-converge, a Claude Code skill
146
+ > that... [first sentence describes what]. Recent work adds mergeability
147
+ > gates, GitHub Copilot reviewer integration, and post-convergence Copilot
148
+ > follow-up across three open pull requests (11,000+ lines)."
149
+
150
+ > **Fix:** Keep the capability sentence. Move the second sentence to a cover
151
+ > letter or interview talking point.
152
+
153
+ ## Review protocol
154
+
155
+ Walk this protocol against every bullet on the resume. Mark each bullet
156
+ PASS, MINOR, or MAJOR.
157
+
158
+ ### Step 1: Scope check
159
+
160
+ Read the bullet aloud in 4 seconds or less. If you cannot finish in 4
161
+ seconds, the bullet is too long. **Action:** trim to one sentence under 25
162
+ words unless the bullet carries a justified centerpiece (and centerpiece
163
+ bullets stay under 40 words).
164
+
165
+ ### Step 2: Offense scan
166
+
167
+ For each bullet, scan for the eight offense categories above. Record any
168
+ hits with the category number.
169
+
170
+ ### Step 3: Verify proof points
171
+
172
+ Every numeric claim in a bullet must be verifiable. If a number cannot be
173
+ sourced, drop it. Hedging numbers ("roughly", "around", "approximately")
174
+ are acceptable when the underlying number is verifiable but variable.
175
+
176
+ ### Step 4: Audience alignment
177
+
178
+ For each bullet, ask: would the hiring manager for **this specific role**
179
+ recognize the terms used? If the bullet uses domain language outside the
180
+ target role's vocabulary (engineering jargon on a trust-and-safety resume,
181
+ finance acronyms on an engineering resume), rewrite using domain-neutral
182
+ capability nouns.
183
+
184
+ ### Step 5: Mechanism removal pass
185
+
186
+ For each bullet that survived steps 1-4, ask: does this bullet describe
187
+ **what** the work accomplished, or **how** the work was done? If it
188
+ describes **how**, rewrite to describe **what** and move the **how** to
189
+ interview prep notes.
190
+
191
+ ### Step 6: Section-level coherence
192
+
193
+ After per-bullet review, scan each section. Check for:
194
+
195
+ - **Bullet count consistency** — sections with similar weight should have
196
+ similar bullet counts. A 5-bullet section next to a 1-bullet section
197
+ signals imbalance.
198
+ - **Voice consistency** — verb tense, sentence structure, and bullet length
199
+ should match across sections.
200
+ - **Relevance ordering** — the most-relevant-to-the-target-role section
201
+ should appear first within its time band (Steve Dalton, *The 2-Hour Job
202
+ Search* §6: relevance over strict chronology when chronology does not
203
+ conflict).
204
+
205
+ ## Output format
206
+
207
+ The audit produces a markdown report with this structure:
208
+
209
+ ```markdown
210
+ # Resume Audit Report
211
+
212
+ ## Top offenders (worst first)
213
+
214
+ ### #1 — [Section] bullet [N]. [Severity]
215
+
216
+ [Quote of current bullet]
217
+
218
+ **Offenses:** [comma-separated category numbers]
219
+
220
+ **Proposed rewrite:** [tighter version]
221
+
222
+ **Words saved:** [N words → M words]
223
+
224
+ (repeat for each offender)
225
+
226
+ ## Sections that pass
227
+
228
+ - [Section name]: [bullet count] bullets, all PASS
229
+ - (repeat)
230
+
231
+ ## Section-level findings
232
+
233
+ [Coherence issues from Step 6, if any]
234
+
235
+ ## Sources cited
236
+
237
+ - Bock, *Work Rules!* (2015), p. 71 — for offenses [N]
238
+ - Dalton, *2-Hour Job Search* (2012), Ch. 3 — for offenses [N]
239
+ - Adler, *Hire With Your Head* (2021) — for offenses [N]
240
+ ```
241
+
242
+ ## When this skill applies
243
+
244
+ - User asks to review or audit a resume
245
+ - User asks "is my resume too detailed"
246
+ - User asks for tighter resume bullets
247
+ - A resume document is shared and the user wants feedback
248
+ - Before submitting a resume to a target role
249
+
250
+ ## When this skill does not apply
251
+
252
+ - Cover letter review (use a separate cover-letter-review skill if needed)
253
+ - Resume formatting fixes (font, spacing, layout) — this skill audits
254
+ content only
255
+ - Resume creation from scratch — this skill assumes a draft exists
256
+
257
+ ## Triggers
258
+
259
+ `/resume-review`, "review my resume", "audit this resume", "is my resume
260
+ too detailed", "tighten my resume bullets", "what offenses do you see in my
261
+ resume", "are my bullets too granular".
@@ -1,58 +0,0 @@
1
- # Bugteam utility scripts
2
-
3
- Scripts in this directory are **executed** by the lead or teammates. They are not loaded into context as instructions (see Anthropic [Skill authoring best practices — Progressive disclosure](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#progressive-disclosure-patterns)).
4
-
5
- | Script | Purpose |
6
- |--------|---------|
7
- | `bugteam_preflight.py` | Run pytest (when configured) and optional `pre-commit` before `/bugteam`. |
8
- | `bugteam_fix_hookspath.py` | Auto-remediate a stale local `core.hooksPath` override, set canonical global value, re-run `bugteam_preflight.py`. Invoked by Claude when preflight reports a `core.hooksPath` failure. |
9
- | `bugteam_code_rules_gate.py` | Run `validate_content` from `code-rules-enforcer.py` on PR-scoped files (`git diff` vs merge-base). Exit `1` if any mandatory rule fails. Invoked **before each audit**; the fixer clears it before the auditor runs. |
10
- | `grant_project_claude_permissions.py` | Idempotent grant of Edit/Write/Read on `cwd/.claude/**` into `~/.claude/settings.json`. |
11
- | `revoke_project_claude_permissions.py` | Removes the matching grant entries from `~/.claude/settings.json`. |
12
- | `test_claude_permissions_common.py` | Pytest module for path normalization and glob-metacharacter guards in `_claude_permissions_common.py`. |
13
- | `_claude_permissions_common.py` | Shared helpers for the grant/revoke scripts (atomic JSON writes, settings sections). |
14
-
15
- ## `bugteam_preflight.py`
16
-
17
- From the repository root:
18
-
19
- ```bash
20
- python "${CLAUDE_SKILL_DIR}/scripts/bugteam_preflight.py"
21
- ```
22
-
23
- - Skips pytest when `BUGTEAM_PREFLIGHT_SKIP=1`.
24
- - Skips pytest when `pytest.ini` / `pyproject.toml` exists but no `test_*.py` / `*_test.py` files are found under the repo root.
25
- - Pytest exit code `5` (no tests collected) is treated as success.
26
- - Add `--pre-commit` to run `pre-commit run --all-files` when `.pre-commit-config.yaml` exists.
27
-
28
- ## `bugteam_fix_hookspath.py`
29
-
30
- From the repository root:
31
-
32
- ```bash
33
- python "${CLAUDE_SKILL_DIR}/scripts/bugteam_fix_hookspath.py"
34
- ```
35
-
36
- - Removes any local-scope `core.hooksPath` value that does not end in `hooks/git-hooks`.
37
- - Sets `git config --global core.hooksPath ~/.claude/hooks/git-hooks` when the global value is unset or non-canonical.
38
- - Refuses to run (exit non-zero) when `~/.claude/hooks/git-hooks` does not exist on disk — install via `npx claude-dev-env .` first.
39
- - Idempotent: a second invocation is a clean no-op.
40
- - Re-runs `bugteam_preflight.py --no-pytest` and propagates its exit code.
41
-
42
- The bugteam SKILL invokes this automatically when preflight stderr indicates a `core.hooksPath` failure, so Claude does not surface the error to the user.
43
-
44
- ## `bugteam_code_rules_gate.py`
45
-
46
- From the repository root (same merge-base rules as the PR head vs base — default `--base origin/main`):
47
-
48
- ```bash
49
- python "${CLAUDE_SKILL_DIR}/scripts/bugteam_code_rules_gate.py"
50
- ```
51
-
52
- Optional explicit files instead of `git diff`:
53
-
54
- ```bash
55
- python "${CLAUDE_SKILL_DIR}/scripts/bugteam_code_rules_gate.py" path/to/a.py path/to/b.ts
56
- ```
57
-
58
- This loads `validate_content` from `hooks/blocking/code-rules-enforcer.py` inside `claude-dev-env` (same logic as the PreToolUse hook). Exit `0` = mandatory checks pass on scanned files; exit `1` = violations printed to stderr.
@@ -1,219 +0,0 @@
1
- """Shared helpers for grant_project_claude_permissions and revoke_project_claude_permissions.
2
-
3
- Writes to ~/.claude/settings.json are atomic and permission-preserving: the
4
- target file's existing POSIX mode is captured, a sibling temp file is
5
- created via os.open with O_CREAT | O_EXCL and the preserved mode, content
6
- is written, then os.replace swaps it into place. Output is serialized with
7
- sort_keys=True for a stable on-disk layout; the first run on a hand-ordered
8
- settings file produces a one-time re-sort diff, subsequent writes are stable.
9
- """
10
-
11
- import json
12
- import os
13
- import stat
14
- import sys
15
- from pathlib import Path
16
- from typing import NoReturn
17
-
18
-
19
- TEXT_FILE_ENCODING: str = "utf-8"
20
- PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Write", "Read")
21
-
22
- AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE: str = (
23
- "Trusted local workspace: {project_path}/.claude/** is the user's "
24
- "project Claude Code config tree; edits inside are routine"
25
- )
26
-
27
-
28
- def exit_with_error(message: str) -> NoReturn:
29
- print(f"Error: {message}", file=sys.stderr)
30
- raise SystemExit(1)
31
-
32
-
33
- def path_contains_glob_metacharacters(candidate_path: str) -> bool:
34
- glob_metacharacters_in_path: tuple[str, ...] = (
35
- "*",
36
- "?",
37
- "[",
38
- "]",
39
- "(",
40
- ")",
41
- "{",
42
- "}",
43
- ",",
44
- )
45
- return any(
46
- each_character in candidate_path
47
- for each_character in glob_metacharacters_in_path
48
- )
49
-
50
-
51
- def get_current_project_path() -> str:
52
- normalized_project_path = str(Path.cwd()).replace("\\", "/")
53
- if path_contains_glob_metacharacters(normalized_project_path):
54
- raise ValueError(
55
- f"Current directory path contains glob metacharacters and cannot "
56
- f"be used to build permission rules safely: {normalized_project_path}"
57
- )
58
- return normalized_project_path
59
-
60
-
61
- def build_permission_rule(tool_name: str, project_path: str) -> str:
62
- return f"{tool_name}({project_path}/.claude/**)"
63
-
64
-
65
- def build_permission_rules(
66
- project_path: str, permission_allow_tools: tuple[str, ...]
67
- ) -> list[str]:
68
- return [
69
- build_permission_rule(each_tool, project_path)
70
- for each_tool in permission_allow_tools
71
- ]
72
-
73
-
74
- def load_settings(settings_path: Path) -> dict[str, object]:
75
- if not settings_path.exists():
76
- return {}
77
- parsed_settings: dict[str, object] = {}
78
- try:
79
- raw_text = settings_path.read_text(encoding=TEXT_FILE_ENCODING)
80
- except OSError as read_error:
81
- exit_with_error(f"Failed to read {settings_path}: {read_error}")
82
- try:
83
- parsed_settings = json.loads(raw_text)
84
- except json.JSONDecodeError as decode_error:
85
- exit_with_error(
86
- f"Refusing to modify {settings_path}: existing file is not valid JSON "
87
- f"({decode_error}). Fix or back up the file manually, then re-run."
88
- )
89
- if not isinstance(parsed_settings, dict):
90
- exit_with_error(
91
- f"Refusing to modify {settings_path}: existing file's root is "
92
- f"{type(parsed_settings).__name__}, not a JSON object. Fix or back up "
93
- f"the file manually, then re-run."
94
- )
95
- return parsed_settings
96
-
97
-
98
- def serialize_settings_to_json_text(settings: dict[str, object]) -> str:
99
- json_indent_width_columns: int = len(" ")
100
- return json.dumps(
101
- settings,
102
- indent=json_indent_width_columns,
103
- sort_keys=True,
104
- )
105
-
106
-
107
- def get_mode_to_preserve(settings_path: Path) -> int:
108
- default_settings_file_mode: int = 0o600
109
- try:
110
- stat_result = os.stat(settings_path)
111
- except FileNotFoundError:
112
- return default_settings_file_mode
113
- except OSError as stat_error:
114
- exit_with_error(f"Failed to stat {settings_path}: {stat_error}")
115
- return stat.S_IMODE(stat_result.st_mode)
116
-
117
-
118
- def write_atomically_with_mode(
119
- temporary_path: Path, serialized_content: str, file_mode: int
120
- ) -> None:
121
- file_descriptor = os.open(
122
- str(temporary_path),
123
- os.O_WRONLY | os.O_CREAT | os.O_EXCL,
124
- file_mode,
125
- )
126
- with os.fdopen(file_descriptor, "w", encoding=TEXT_FILE_ENCODING) as writer:
127
- writer.write(serialized_content)
128
-
129
-
130
- def save_settings(settings_path: Path, settings: dict[str, object]) -> None:
131
- atomic_write_temporary_suffix: str = ".tmp"
132
- settings_path.parent.mkdir(parents=True, exist_ok=True)
133
- serialized_settings = serialize_settings_to_json_text(settings)
134
- temporary_path = settings_path.with_suffix(
135
- settings_path.suffix + atomic_write_temporary_suffix
136
- )
137
- mode_to_preserve = get_mode_to_preserve(settings_path)
138
- try:
139
- try:
140
- write_atomically_with_mode(
141
- temporary_path, serialized_settings, mode_to_preserve
142
- )
143
- os.replace(str(temporary_path), str(settings_path))
144
- except OSError as os_error:
145
- exit_with_error(
146
- f"Failed to write settings atomically to {settings_path}: {os_error}"
147
- )
148
- finally:
149
- if temporary_path.exists():
150
- try:
151
- temporary_path.unlink()
152
- except OSError:
153
- pass
154
-
155
-
156
- def append_if_missing(target_list: list[object], new_value: str) -> bool:
157
- if new_value in target_list:
158
- return False
159
- target_list.append(new_value)
160
- return True
161
-
162
-
163
- def ensure_dict_section(
164
- settings: dict[str, object], section_name: str
165
- ) -> dict[str, object]:
166
- """Return an existing dict section or create an empty one if absent.
167
-
168
- A missing key and an explicit JSON null are treated identically: both
169
- produce a fresh empty dict stored back into settings. Any other non-dict
170
- value (string, list, number, bool) calls exit_with_error to avoid
171
- overwriting user data.
172
- """
173
- existing_section = settings.get(section_name)
174
- if existing_section is None:
175
- replacement_section: dict[str, object] = {}
176
- settings[section_name] = replacement_section
177
- return replacement_section
178
- if not isinstance(existing_section, dict):
179
- exit_with_error(
180
- f"Refusing to modify settings key {section_name!r}: existing value "
181
- f"is {type(existing_section).__name__}, not a JSON object. Fix or "
182
- f"remove the key manually, then re-run."
183
- )
184
- return existing_section
185
-
186
-
187
- def ensure_list_entry(section: dict[str, object], entry_name: str) -> list[object]:
188
- """Return an existing list entry or create an empty one if absent.
189
-
190
- A missing key and an explicit JSON null are treated identically: both
191
- produce a fresh empty list stored back into the section. Any other
192
- non-list value (string, dict, number, bool) calls exit_with_error to
193
- avoid overwriting user data.
194
- """
195
- existing_entry = section.get(entry_name)
196
- if existing_entry is None:
197
- replacement_entry: list[object] = []
198
- section[entry_name] = replacement_entry
199
- return replacement_entry
200
- if not isinstance(existing_entry, list):
201
- exit_with_error(
202
- f"Refusing to modify settings entry {entry_name!r}: existing value "
203
- f"is {type(existing_entry).__name__}, not a JSON array. Fix or "
204
- f"remove the entry manually, then re-run."
205
- )
206
- return existing_entry
207
-
208
-
209
- def prune_empty_list_then_empty_section(
210
- settings: dict[str, object], section_key: str, list_key: str
211
- ) -> None:
212
- section = settings.get(section_key)
213
- if not isinstance(section, dict):
214
- return
215
- list_entry = section.get(list_key)
216
- if isinstance(list_entry, list) and len(list_entry) == 0:
217
- del section[list_key]
218
- if len(section) == 0:
219
- del settings[section_key]