fix(#2278): use Edit() not Write() for Claude allow-permissions + migrate legacy (#2302)

GSD_CLAUDE_ALLOW_PERMISSIONS pre-populated Claude Code settings.json
with Write(.planning/*) and Write(STATE.md). Claude Code has no
standalone Write permission gate — file-editing tools are gated
collectively via Edit(pattern) — so those rules never matched, fresh
installs still hit first-run approval prompts for .planning/* and
STATE.md, and Claude Code emitted a session-start warning about the
unmatched rules.

Swap the two entries to Edit(.planning/*) / Edit(STATE.md). Add a
GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS list of the retired Write(...) forms,
consulted by mergeClaudePermissions (actively remove stale entries when
adding current ones, idempotent, user entries preserved) and by the
uninstall cleanup filter (still removes the legacy form). Sample
settings.json in docs/USER-GUIDE.md corrected to match.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-15 13:46:26 -04:00
committed by GitHub
parent f74442310d
commit 4a9833d3e3
4 changed files with 207 additions and 12 deletions

View File

@@ -168,15 +168,28 @@ const DEFAULT_RUNTIME = 'claude';
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
'Bash(npx gsd-core *)',
'Read(.planning/*)',
'Write(.planning/*)',
'Edit(.planning/*)',
'Read(STATE.md)',
'Write(STATE.md)',
'Edit(STATE.md)',
]);
const GSD_CLAUDE_DENY_PERMISSIONS = Object.freeze([
'Read(.env)',
'Read(.env.*)',
'Read(.secrets)',
]);
// #2278 — Stale allow-rule forms from before the fix. Claude Code has no
// standalone `Write` permission gate: file-editing tools (Write/Edit/
// NotebookEdit) are gated collectively via `Edit(pattern)`. The original
// `Write(.planning/*)` / `Write(STATE.md)` entries were therefore silently
// unmatched (never granted anything) and Claude Code additionally surfaces a
// session-start warning about unmatched permission rules. This list lets
// mergeClaudePermissions and uninstall cleanup retire those stale entries on
// existing installs while the current GSD_CLAUDE_ALLOW_PERMISSIONS above
// carries the working `Edit(...)` forms.
const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
'Write(.planning/*)',
'Write(STATE.md)',
]);
/**
* Merge GSD-owned permission entries into a Claude Code settings object.
@@ -185,6 +198,12 @@ const GSD_CLAUDE_DENY_PERMISSIONS = Object.freeze([
* entries are appended only if not already present. No other permission sub-keys
* (ask, disableBypassPermissionsMode, etc.) are touched.
*
* Migration (#2278): before adding the current GSD_CLAUDE_ALLOW_PERMISSIONS,
* any stale GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS entry (e.g. the unmatched
* `Write(...)` forms from before the fix) is removed from permissions.allow,
* so existing installs end up with the working `Edit(...)` forms instead of
* both the dead legacy entry and its replacement sitting side by side.
*
* Defensive: if settings is not a plain object, returns immediately without
* throwing. If permissions.allow / permissions.deny exist but are not arrays
* (malformed settings), they are replaced with valid arrays.
@@ -205,6 +224,10 @@ function mergeClaudePermissions(settings) {
settings.permissions.deny = [];
}
settings.permissions.allow = settings.permissions.allow.filter(
(e) => !GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS.includes(e)
);
for (const entry of GSD_CLAUDE_ALLOW_PERMISSIONS) {
if (!settings.permissions.allow.includes(entry)) {
settings.permissions.allow.push(entry);
@@ -7591,8 +7614,11 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
let permissionsModified = false;
if (Array.isArray(settings.permissions.allow)) {
const before = settings.permissions.allow.length;
// #2278 — filter against the union of the current allow-rule forms
// AND the retired legacy forms, so uninstall still cleans up
// pre-fix installs that still carry the stale `Write(...)` entries.
settings.permissions.allow = settings.permissions.allow.filter(
(e) => !GSD_CLAUDE_ALLOW_PERMISSIONS.includes(e)
(e) => !GSD_CLAUDE_ALLOW_PERMISSIONS.includes(e) && !GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS.includes(e)
);
if (settings.permissions.allow.length !== before) {
permissionsModified = true;
@@ -12157,6 +12183,7 @@ module.exports = {
// #768 — Claude Code permissions pre-population
mergeClaudePermissions,
GSD_CLAUDE_ALLOW_PERMISSIONS,
GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS,
GSD_CLAUDE_DENY_PERMISSIONS,
GSD_CODEX_MARKER,
CODEX_AGENT_SANDBOX,