feat(setup): track ai/claude/settings.json template + per-key merge (SDD-002)#51
Merged
Merged
Conversation
…SDD-002) Tier 3 of the 5-layer SDD enforcement stack started by SDD-001 (PR #49). Closes two gaps: 1. No SSOT for "what does dotfiles own in settings.json" -- the hook entry was hardcoded as a PowerShell hashtable in setup-windows.ps1 and as a bash heredoc in setup-linux.sh. Adding a new structural key required editing both scripts in parallel -- the exact duplication class that caused WIN-003 + BUG-002 + BUG-003. 2. Fresh-machine doble-paso: without ~/.claude/settings.json, both scripts logged "settings.json not found, skipping" and left the hook unregistered. User had to run `claude` once (to create the default), then re-run setup. What this PR does: - New `ai/claude/settings.json` template (42 lines, plain JSON). Contains the curated "ours" subset: model=opus, effortLevel=xhigh, hooks.SessionStart (with __HOOK_COMMAND__ placeholder), enabledPlugins (the 14 universal plugins), permissions.allow (only 3 portable MCP entries; no Read paths). - `setup-windows.ps1`: new `Merge-ClaudeSettings` helper function (next to existing Write-* / Ensure-Directory helpers). The previous 45-line inline hashtable hook-registration block becomes a single function call. Bulk copy of ai/claude/* now excludes settings.json explicitly (was a latent collision -- would have deployed the template verbatim with placeholder). - `setup-linux.sh`: equivalent `merge_claude_settings` shell function using jq with --argjson for the per-key merge. Bulk cp loop now skips settings.json. The previous 24-line inline HOOK_ENTRY heredoc + jq merge becomes a single function call. - Bootstrap branch in both scripts: when ~/.claude/settings.json does not exist, setup writes the template (with __HOOK_COMMAND__ substituted) directly. Closes the doble-paso. Logs "Bootstrapping ~/.claude/settings.json from template (file did not exist)". Per-key merge policy (table in specs/SDD-002-settings-portability/proposal.md): - model, effortLevel: TEMPLATE wins - permissions.allow: UNION (template + existing, deduped) -- preserves user's machine-specific Read paths AND adds our 3 portable MCP entries - permissions.additionalDirectories / .deny / other subkeys: USER preserved - hooks.SessionStart: TEMPLATE wins (replace entire array) -- this is the one we own - hooks.PreToolUse / PostToolUse / Stop: USER preserved -- third-party tools (claude-mem, GitGuardian) register hooks here - enabledPlugins: object merge (template wins on conflict; user-added plugins beyond the 14 universal ones survive) - All other top-level keys: USER preserved (future-proof) Verification (empirical, this machine, 2026-05-18): - Merge smoke: pre-state had permissions.allow count=6 (3 MCPs + 3 Read paths), additionalDirectories=1, hooks.SessionStart=1, 14 plugins. Post-merge: every count identical (UNION dedup correctly kept all 6, not 9), hooks.SessionStart command substituted to actual hook path. - Bootstrap smoke: moved settings.json to .bootstrap-test, re-ran setup, log emitted "Bootstrapping ~/.claude/settings.json from template (file did not exist)" + "Claude settings.json bootstrapped from template". New file contained the template content with __HOOK_COMMAND__ correctly replaced. Original restored. - PSScriptAnalyzer clean (the expected PSUseSingularNouns warning for `Merge-ClaudeSettings` is suppressed in-line with explicit rationale: "Settings" is the canonical Claude Code config-file name; singular `Setting` would be misleading). - `bash -n setup-linux.sh` clean. - All 18 + 11 = 29 new bats asserts simulated locally via jq + grep, 100% green. Tests (additive): - `tests/claude-settings-template.bats` (new file, 18 asserts): template exists, valid JSON, contains required keys, has placeholder, 14 plugins, 3 MCP entries with no Read paths, no additionalDirectories, no PreToolUse / PostToolUse / Stop. - `tests/setup-windows.bats` SDD-002 parity section (11 asserts): both scripts reference the template path, define their merge function, call it (not inline), exclude settings.json from bulk-copy, log the bootstrap message, substitute __HOOK_COMMAND__. Spec scaffolding: - `specs/SDD-002-settings-portability/` with proposal + tasks + verification.md, all filled with substantive content. Eat-our-dog-food: vault entry added BEFORE branch + init-spec scaffold BEFORE code, per the discipline gate instituted in SDD-001 (PR #49). Out of scope (deferred): - SDD-003: CI spec-gate workflow + PR template (PR-time enforcement layer). - Cross-machine variable substitution beyond __HOOK_COMMAND__ (e.g., user path placeholders) -- machine-specific paths stay user-owned. - Promotion of the user/machine-specific keys (Read paths, additionalDirectories) into the template -- they are intentionally user-owned by the policy.
…r + add -- separator PR #51 CI caught three test failures: 1. setup-windows.bats "SessionStart hook self-heals on path drift": asserted the old `$existingHookCommand -eq $expectedHookCommand` compare-then-rewrite pattern, gone after SDD-002 moved hook registration into Merge-ClaudeSettings. 2. setup-linux.bats same test: asserted the old `EXISTING_HOOK_COMMAND` / `EXPECTED_HOOK_COMMAND` jq-then-rewrite, gone after the merge_claude_settings refactor. 3. setup-windows.bats "SDD-002 ... -Exclude 'settings.json'": grep -qF misparsed the pattern starting with `-Exclude` as a flag. Fixed with `--` separator (same fix pattern as the BUG-002 CORE PRINCIPLE assert). Self-heal semantic is PRESERVED in both setup scripts (in fact strengthened -- the merge function always rewrites hooks.SessionStart from the template, not just on detected drift). Tests updated to assert the new mechanism. Pre-flight audit would have caught these locally if I had grep-simulated the exact bats assertions instead of paraphrasing. Lesson for future refactors: when changing implementation shape of a function with bats parity asserts, grep the exact assertion patterns against the new code BEFORE pushing.
This was referenced May 18, 2026
mlorentedev
added a commit
that referenced
this pull request
May 19, 2026
…werShell 5.1 (BUG-005) SDD-002 (PR #51) introduced Merge-ClaudeSettings which uses `ConvertFrom-Json -AsHashtable` -- a parameter added in PowerShell 7.0 that does NOT exist in Windows PowerShell 5.1. The natural Windows command `PowerShell -ExecutionPolicy Bypass -File .\setup-windows.ps1` resolves `PowerShell` to 5.1 on a stock Windows machine, the Merge function's wide try/catch swallows the ParameterBindingException as if it were a JSON parse error ("not valid JSON after placeholder substitution: A parameter cannot be found that matches parameter name 'AsHashtable'"), and the per-key merge from ai/claude/settings.json is silently skipped. Existing ~/.claude/settings.json survives untouched, so the bug is invisible until a fresh-machine bootstrap depends on the template merge. Implementation: preamble block after param() and before CONFIGURATION header detects $PSVersionTable.PSVersion.Major < 7. If `Get-Command pwsh` resolves to PowerShell 7+, re-exec under it with `-NoProfile -ExecutionPolicy Bypass -File $PSCommandPath @args` and exit with the child's exit code. Otherwise, print an actionable [ERROR] line pointing at `winget install Microsoft.PowerShell` and exit 1 (fail-loud, no silent skip). Verification: - Smoke under Windows PowerShell 5.1.22621.6931: first output line is `[INFO] Windows PowerShell 5.1.22621.6931 detected; re-executing under pwsh (...) for full feature compatibility (BUG-005)`. Setup proceeds under pwsh, settings.json merged correctly (1687 bytes, 14 plugins + preserved user customizations), zero AsHashtable warnings. - Smoke under pwsh 7.6.1 direct: preamble no-op, no `[INFO] ... detected` lines, existing behavior preserved (verified during BUG-004 smoke same day). - pwsh-missing branch: not directly testable (pwsh installed on dev machine); structurally locked by bats grep asserting `winget install Microsoft.PowerShell` literal + `exit 1`. - 5 new bats asserts in tests/setup-windows.bats; RED -> GREEN via grep-by-grep emulation. - PSScriptAnalyzer: 4 new Write-Host warnings, style-consistent with existing Write-Info/Success/Warn/Err wrappers (those wrappers are themselves Write-Host calls; preamble cannot use them because the wrappers are defined below the preamble's runtime position). Zero Errors. Zero new rule classes. Linux side (setup-linux.sh) is immune by construction (bash + jq, no shell-version coupling); zero changes there. Negative-parity bats assert locks this in. Spec: specs/BUG-005-setup-ps7-reexec/ Vault: 10_projects/dotfiles/11-tasks.md (entry added in same session) Sibling: PR #57 (BUG-004 claude-mem-truncate-guard) Upstream: https://learn.microsoft.com/powershell/scripting/whats-new/what-s-new-in-powershell-70 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tier 3 of the 5-layer SDD enforcement stack started by SDD-001 (#49). Adds a canonical
ai/claude/settings.jsontemplate as the SSOT for the "dotfiles-owned" subset of~/.claude/settings.json, refactors both setup scripts to read it via aMerge-ClaudeSettings/merge_claude_settingshelper, and bootstraps the target on fresh machines (closes the doble-paso friction).Why
Hook registration logic was duplicated in
setup-windows.ps1(PowerShell hashtable) andsetup-linux.sh(bash heredoc) -- the exact duplication class that caused WIN-003 + BUG-002 + BUG-003. Also, on fresh machines without~/.claude/settings.json, both scripts logged "settings.json not found, skipping" and required the user to runclaudeonce then re-run setup.Per-key merge policy (the heart of this PR)
model,effortLevelpermissions.allowpermissions.additionalDirectories/ othershooks.SessionStarthooks.PreToolUse/PostToolUse/StopenabledPluginsFull proposal + reasoning at
specs/SDD-002-settings-portability/proposal.md.Eat-our-dog-food
Followed the SDD Discipline Gate (instituted in PR #49) from the first commit:
10_projects/dotfiles/11-tasks.mdSDD-002 entry (auto-synced)scripts/init-spec.ps1 SDD-002-settings-portabilityscaffolded the spec folder BEFORE any codeTest plan
permissions.allowentries before -> 6 after, including 3 absolute-path Read entries the template doesn't touch);additionalDirectoriespreserved untouched;hooks.SessionStartrewritten with__HOOK_COMMAND__correctly substituted to actual path; 14enabledPluginspreservedPSUseSingularNounsforMerge-ClaudeSettingsis suppressed in-line with explicit rationale)bash -n setup-linux.sh: cleanOut of scope (deferred)
.github/workflows/ci.ymlspec-gatejob +.github/PULL_REQUEST_TEMPLATE.mdSDD checklist. PR-time hard enforcement, Tier 4+5 of the SDD-001 stack.__HOOK_COMMAND__. Machine-specific paths (Read entries,additionalDirectories) stay user-owned by the merge policy.merge_claude_settingsshell function mirrors the PowerShell logic line-for-line.Cross-references
10_projects/dotfiles/11-tasks.mdSDD-002-settings-portability entry