Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wise-voles-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
type: Fixed
pr: 5269
---
**Claude Code installs no longer pre-authorize `npx gsd-core`** — the installer wrote a `Bash(npx gsd-core *)` allow rule, but GSD ships as `@opengsd/gsd-core` and the unscoped npm name belongs to a different owner, so Claude Code would run whatever was published under it without asking. Fresh installs no longer write the rule, the next install or upgrade removes it from an existing settings file (exact match only), and `GEMINI.md` now gives the scoped install command. (#5054)
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ Module owning the `{"type":"commonjs"}` module-type marker GSD writes beside its
Module owning validation for Installer Migration Module records and planned actions. It enforces migration metadata, explicit install scopes, ownership evidence for destructive/config actions, and runtime contract citations for runtime config rewrites before a migration can enter planning or apply.

### Installer Module
Primary installer for all runtimes. Single production file: `bin/install.js` (hand-authored JS — it is NOT generated from `src/*.cts`; ADR-1508 keeps it hand-authored deliberately, and no `npm run build` step emits it). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand, configuredEntrypoints }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (18 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, hermes, kimi, kimi-code, kilo, opencode, pi, qwen, trae, windsurf, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `gsd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one GSD's `gsd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/gsd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends GSD-owned allow entries (see `GSD_CLAUDE_ALLOW_PERMISSIONS`) to a Claude Code settings object and filters out the retired legacy forms (`GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS` #2278; `GSD_CLAUDE_LEGACY_DENY_PERMISSIONS` #4221 — the `Read(.env*)` deny rules are retired in favor of the managed `gsd-secret-read-guard.js` hook, and an emptied `deny` array is deleted); called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `gsd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `gsd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `<router>/skills/<name>/SKILL.md` (hermes prefix='': `skills/gsd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/gsd-<stem>/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. `finishInstall` calls `assertConfiguredEntrypoints` (Runtime Hooks Surface Module, dedup'd by `(configPath, scriptPath)` first) before its own `writeSettings` — a failing settings-json-surface install/update never persists that write. `installAllRuntimes`'s `finalize()` runs the same check once over the aggregate set before its `printSummaries()` loop calls `finishInstall` per runtime, so the per-runtime slice is checked twice; the second pass is a dozen-odd `statSync`/`accessSync` calls and is left unconditional rather than gated on a caller-supplied bypass flag. Codex/Cursor/Windsurf/Kimi/Cline writers persist their own hooks.json/config.toml/`.clinerules/hooks/PreToolUse` directly inside `install()`, ahead of `installAllRuntimes`'s aggregate `assertConfiguredEntrypoints` call, so a validation failure there is reported (and blocks the completion message) before that file is reverted where a rollback exists. Codex's `install()` result exposes two rollbacks: `rollbackInstallerMigrations` — like every other runtime's — is the installer-migrations-only closure the name describes, and the Codex-only `rollbackPreInstallSnapshot` binds `restoreCodexSnapshot` (#3245's full pre-install snapshot restore — config.toml, hooks.json, skills/, agents/, VERSION). `installAllRuntimes`'s `rollbackFinalizedInstallerMigrations` picks `rollbackPreInstallSnapshot` only when the triggering error is tagged `configuredEntrypointValidation` (set by `assertConfiguredEntrypoints`); any other finalize-stage exception gets `rollbackInstallerMigrations`, so an unrelated sibling failure (e.g. EACCES on another runtime's permission write) cannot un-install a Codex install that already succeeded. The discriminator is the error's KIND, not which runtime owns the bad entrypoint: the aggregate gate is all-or-nothing, so ANY runtime's invalid entrypoint reverts the Codex snapshot — including when Codex's own entrypoints were fine and its "Done!" summary already printed (#4249). Cursor/Windsurf/Kimi/Cline have no equivalent snapshot, so their result binds only the narrow rollback and a validation failure leaves their just-written file unrevertable (#4249). `bannerOpts.configuredEntrypoints` is the only source `finishInstall` validates — a caller other than `installAllRuntimes` that omits it gets no entrypoint validation at all.
Primary installer for all runtimes. Single production file: `bin/install.js` (hand-authored JS — it is NOT generated from `src/*.cts`; ADR-1508 keeps it hand-authored deliberately, and no `npm run build` step emits it). Exports: `install(isGlobal, runtime[, configDir])` → typed result `{ runtime, configDir, settingsPath, settings, statuslineCommand, updateBannerCommand, configuredEntrypoints }`; `uninstall(isGlobal, runtime[, configDir])`; `installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile)`; `uninstallRuntimeArtifacts(runtime, configDir, scope)`; `writeManifest(configDir, runtime)`. Runtime enum: `allRuntimes` (18 values: claude, antigravity, augment, cline, codebuddy, codex, copilot, cursor, hermes, kimi, kimi-code, kilo, opencode, pi, qwen, trae, windsurf, zcode). Directory helpers: `getDirName(runtime)` → local dir name; `getConfigDirFromHome(runtime, isGlobal)` → shell-quoted path fragment. Per-runtime global config-dir resolution is delegated to `gsd-core/bin/lib/runtime-homes.cjs:getGlobalConfigDir(runtime[, explicitDir])` — the canonical, env-var–aware projection (`explicitDir` override + opencode/kilo `*_CONFIG` file-path precedence); the legacy in-installer `getGlobalDir`/`getOpencodeGlobalDir`/`getKiloGlobalDir` were retired into it (#56). The same module exposes `detectAntigravityDirAmbiguity(opts)` — a side-effect-free probe reporting whether multiple `~/.gemini/antigravity{,-ide,-cli}` dirs coexist and which one GSD's `gsd-core/VERSION` marker (the `dot-home-nested` `probeExists`) resolves to, for installer / `/gsd-update` operator guidance when a pre-#217 install landed in the wrong sibling dir (#1441). Runtime-specific helpers: `resolveKiloConfigPath(configDir)`, `configureKiloPermissions(isGlobal[, explicitDir])`. Claude-specific permission helpers: `mergeClaudePermissions(settings)` — non-destructively appends GSD-owned allow entries (see `GSD_CLAUDE_ALLOW_PERMISSIONS`) to a Claude Code settings object and filters out the retired legacy forms (`GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS` #2278 — also retires the unscoped `Bash(npx gsd-core *)` rule (#5054); `GSD_CLAUDE_LEGACY_DENY_PERMISSIONS` #4221 — the `Read(.env*)` deny rules are retired in favor of the managed `gsd-secret-read-guard.js` hook, and an emptied `deny` array is deleted); called from `finishInstall` for `runtime === 'claude'` only; uninstall removes exactly these entries (#768). Layout-driven artifact copy/removal delegates to `gsd-core/bin/lib/runtime-artifact-layout.cjs:resolveRuntimeArtifactLayout` (throws `TypeError` for unknown runtimes). Five runtimes with non-recursive skill loaders (cline, qwen, hermes, augment, trae) use a nested router layout: 6 `gsd-ns-*` router bundles emitted as top-level skills, with concrete skills nested at `<router>/skills/<name>/SKILL.md` (hermes prefix='': `skills/gsd/ns-*/…`). claude (reverted from nested per #924 — the Skill tool errors on unrouted names) and antigravity (one-level scan, but concrete skills must be top-level discoverable) plus the remaining skills-runtimes (cursor, codex, copilot, windsurf, codebuddy, opencode, kilo) use the flat `skills/gsd-<stem>/` layout. See Skill Surface Budget Module and Runtime Artifact Layout Module. `finishInstall` calls `assertConfiguredEntrypoints` (Runtime Hooks Surface Module, dedup'd by `(configPath, scriptPath)` first) before its own `writeSettings` — a failing settings-json-surface install/update never persists that write. `installAllRuntimes`'s `finalize()` runs the same check once over the aggregate set before its `printSummaries()` loop calls `finishInstall` per runtime, so the per-runtime slice is checked twice; the second pass is a dozen-odd `statSync`/`accessSync` calls and is left unconditional rather than gated on a caller-supplied bypass flag. Codex/Cursor/Windsurf/Kimi/Cline writers persist their own hooks.json/config.toml/`.clinerules/hooks/PreToolUse` directly inside `install()`, ahead of `installAllRuntimes`'s aggregate `assertConfiguredEntrypoints` call, so a validation failure there is reported (and blocks the completion message) before that file is reverted where a rollback exists. Codex's `install()` result exposes two rollbacks: `rollbackInstallerMigrations` — like every other runtime's — is the installer-migrations-only closure the name describes, and the Codex-only `rollbackPreInstallSnapshot` binds `restoreCodexSnapshot` (#3245's full pre-install snapshot restore — config.toml, hooks.json, skills/, agents/, VERSION). `installAllRuntimes`'s `rollbackFinalizedInstallerMigrations` picks `rollbackPreInstallSnapshot` only when the triggering error is tagged `configuredEntrypointValidation` (set by `assertConfiguredEntrypoints`); any other finalize-stage exception gets `rollbackInstallerMigrations`, so an unrelated sibling failure (e.g. EACCES on another runtime's permission write) cannot un-install a Codex install that already succeeded. The discriminator is the error's KIND, not which runtime owns the bad entrypoint: the aggregate gate is all-or-nothing, so ANY runtime's invalid entrypoint reverts the Codex snapshot — including when Codex's own entrypoints were fine and its "Done!" summary already printed (#4249). Cursor/Windsurf/Kimi/Cline have no equivalent snapshot, so their result binds only the narrow rollback and a validation failure leaves their just-written file unrevertable (#4249). `bannerOpts.configuredEntrypoints` is the only source `finishInstall` validates — a caller other than `installAllRuntimes` that omits it gets no entrypoint validation at all.

### I/O Module
Module owning the tool's CLI I/O primitives: `output()` result emission (with large-payload temp-file spillover via `GSD_TEMP_DIR`/`ensureGsdTempDir`/`reapStaleTempFiles`), `error()` stderr emission with exit-code mapping, and the JSON-error-mode toggle (`setJsonErrorMode`/`getJsonErrorMode`, `ERROR_REASON`). **Degraded result vs fault (ADR-2980, #2980):** the two emitters are a deliberate two-channel failure contract, not a drift. A **fault** is `error(message, reason)` — stderr, exit **1**, structured `{ok:false,reason,message}` envelope under `--json-errors`. A **degraded result** is `output({ error: … })` — stdout, exit **0**, `--json-errors` does not apply — and means the command ran to completion and is reporting a condition (absent artifact, and in practice also missing-argument and unusable-input cases) through its result; a caller detects it by inspecting the payload, never by exit code. Ratified across **60 sites in 9 modules** (`state` 25, `verify` 8, `workstream` 7, `frontmatter` 6, `commands` 5, `template` 3, `gsd2-import` 2, `phase` 2, `roadmap` 2) because normalizing them to exit 1 is a Hyrum's Law break over a CRITICAL radius (`get_impact(cmdStateSnapshot)`; `output` has 170 direct callers). #2966/#2980 record "42 sites" — that counts only literals whose FIRST key is `error` (the `output\(\{\s*error:` regex); 18 more put another key first (`{found:false, error}`) and are identical in contract, so 60 is the population and 42 is a subset. New code prefers the fault path or a named-field result (`{updated:false, reason}`), not a 61st site. Known cost carried by the decision: the exit code does not distinguish absent from unusable, which is ADR-1411's "corrupt is not absent" open edge. Docs: `docs/json-errors.md` → "Degraded results vs faults". Extracted from the Core module per ADR-857 rollout phase 1 (#859) so feature modules (`graphify`, `intel`, `audit`, `profile-pipeline`) depend on a small I/O seam instead of the core god-module; the `core.cjs` re-export spine was retired in epic #1267, so callers import this leaf directly. Source of truth: `gsd-core/bin/lib/io.cjs` (generated from `src/io.cts`).
Expand Down
2 changes: 1 addition & 1 deletion GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ files rather than in the conversation.
> run the dedicated installer:
>
> ```bash
> npx gsd-core --antigravity --global
> npx @opengsd/gsd-core@latest --antigravity --global
> ```
>
> The commands below are available only once that installer has run.
Expand Down
9 changes: 7 additions & 2 deletions bin/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function isCodexHooksFeatureKey(key) {
// GSD_CLAUDE_LEGACY_DENY_PERMISSIONS below.)
//
// Format: each string uses Claude Code's documented permission rule syntax \u2014
// "Tool(pattern)" e.g. "Bash(npx gsd-core *)", "Read(.planning/*)"
// "Tool(pattern)" e.g. "Read(.planning/*)", "Edit(STATE.md)"
// "Tool" (bare tool name, no pattern)
//
// Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
Expand All @@ -199,7 +199,6 @@ function isCodexHooksFeatureKey(key) {
// instead of scattered literal 'claude' defaults/rosters (#2086).
const DEFAULT_RUNTIME = 'claude';
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
'Bash(npx gsd-core *)',
'Read(.planning/*)',
'Edit(.planning/*)',
'Read(STATE.md)',
Expand Down Expand Up @@ -232,9 +231,14 @@ const GSD_CLAUDE_LEGACY_DENY_PERMISSIONS = Object.freeze([
// mergeClaudePermissions and uninstall cleanup retire those stale entries on
// existing installs while the current GSD_CLAUDE_ALLOW_PERMISSIONS above
// carries the working `Edit(...)` forms.
// #5054 — `Bash(npx gsd-core *)` is retired the same way. The project ships
// as `@opengsd/gsd-core`; the unscoped `gsd-core` npm name belongs to someone
// else, so the rule pre-authorized whatever that name's owner publishes next.
// GSD never runs the bare form (update.md uses `--package=@opengsd/gsd-core`).
const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
'Write(.planning/*)',
'Write(STATE.md)',
'Bash(npx gsd-core *)',
]);

/**
Expand All @@ -249,6 +253,7 @@ const GSD_CLAUDE_LEGACY_ALLOW_PERMISSIONS = Object.freeze([
* `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.
* The same filter retires the unscoped `Bash(npx gsd-core *)` rule (#5054).
*
* Migration (#4221): the retired GSD_CLAUDE_LEGACY_DENY_PERMISSIONS entries
* are removed from permissions.deny (byte-equal only). Nothing is added to
Expand Down
9 changes: 8 additions & 1 deletion docs/USER-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,6 @@ Since v1.3.1, the installer pre-populates `~/.claude/settings.json` (or
{
"permissions": {
"allow": [
"Bash(npx gsd-core *)",
"Read(.planning/*)",
"Edit(.planning/*)",
"Read(STATE.md)",
Expand All @@ -1015,6 +1014,14 @@ merge is non-destructive — your existing permissions are preserved and GSD ent
are only appended. Uninstalling GSD removes exactly these entries and preserves
any others.

**The unscoped `npx gsd-core` rule is retired (#5054).** Earlier versions also
wrote `Bash(npx gsd-core *)`. GSD is published as `@opengsd/gsd-core`, and the
unscoped `gsd-core` name on npm belongs to a different owner, so that rule let
Claude Code run whatever is published under that name without asking. The next
install or upgrade removes that exact entry; rules you wrote yourself, including
scoped `@opengsd/gsd-core` ones, are kept.
Install and upgrade with `npx @opengsd/gsd-core@latest`.

**Secret-file protection moved from deny rules to a hook (#4221).** Earlier
versions also wrote three `permissions.deny` rules — `Read(.env)`,
`Read(.env.*)` and `Read(.secrets)`. Claude Code 2.1.259 hardened the
Expand Down
Loading
Loading