Skip to content

ADR 0071: The settings seed/merge seam is provider-driven, not a Claude special-case

Status: accepted; extends ADR 0031 (the provider registry as the single source for agent-specific behaviour) and ADR 0040 (the worktree-hook surface)

Context

§

discern seeds one agent's settings file — .claude/settings.json — by deep-merging a bundled template into whatever the project already has (the user's keys are never clobbered; ADR 0031's hook-stripping rides on the same file). That seed/merge path was hardcoded to Claude in three places:

  • template.ts's isSettingsTemplate matched the literal .claude/settings.json.tmpl;
  • fs_plan.ts's planSettingsMerge hardcoded the target .claude/settings.json;
  • the merge itself (settings_merge.ts) assumed JSON.

The next five agents change two of those assumptions. Four (Gemini, Cursor, Copilot, Antigravity) keep settings/hooks in their own JSON file at their own path (.gemini/settings.json, .cursor/…, …); Codex keeps them in TOML. So "settings live in JSON at .claude/settings.json" is Claude's fact, not a universal one — and a later plan that wires any of those agents would have to re-touch the core seed/merge plumbing, or copy-paste it.

Decision

§

The scaffolder routes settings templates by the registry, and the merge strategy is per-provider — so a new hooks provider seeds purely from its registry declaration plus a dropped template.

  • HooksIntegration gains mergeSeed?: SettingsSeedMerge — a text-level strategy (existingText | undefined, incomingText) => string. Text-level, not JSON-object-level, so a non-JSON settings file (Codex's TOML) supplies its own strategy without the core assuming a format. Absent ⇒ the default JSON deep-merge (mergeJsonSettingsText, which lifts the existing mergeSettings to text and is byte-for-byte what Claude produced before).
  • settingsSeeds() derives, from every hooks provider, a SettingsSeed { targetRel: settingsFile, merge: mergeSeed ?? default }. This is the single registry-driven source the scaffolder routes by.
  • buildPlan builds a target → strategy map from settingsSeeds() (injectable for tests) and routes a template to a settings-merge when its target path is a provider's settings file — no .claude/settings.json literal in the core. isSettingsTemplate is deleted (the registry decides now; one source, no dead predicate). template.ts can't import the registry without a cycle, so the routing lives in fs_plan (which already reads both).

The explicit nos:

  • Claude's seeded output stays byte-identical. Its HooksIntegration declares no mergeSeed, so it gets the default JSON strategy, which is the old code path lifted to text.
  • No per-vendor hook format is implemented here. Codex's TOML strategy, Gemini's hooks.enabled, Cursor's and Copilot's shapes are later plans. This generalizes the seam and keeps Claude working.
  • SessionStart-only providers are supported. A non-Claude agent with no worktree create/remove events declares worktreeEventKeys = [] and just a sessionHookNeedle; the hook-stripper and the parity guard both iterate the list, so empty is handled with no special case.

Consequences

§
  • Adding a hooks provider is a declaration + a template. Declare a HooksIntegration (optionally with a mergeSeed) and drop a templates/<settingsFile>.tmplsettingsSeeds() routes it and merges it. No edit to buildPlan, planSettingsMerge, or template.ts. The parity test already asserts each hooks provider has a seed template, so a missing one red-lights the gate.
  • The merge engine is format-pluggable. JSON stays the default for the four JSON-settings agents; Codex's TOML slots in as a strategy later, with the core none the wiser.
  • One less hardcoded agent path. isSettingsTemplate's .claude/settings.json literal is gone; routing now flows from the registry like every other agent-specific path (ADR 0043).
  • The routing decision moved layers. It now lives in fs_plan rather than template.ts, because the token layer cannot import the registry without an import cycle. A small, deliberate placement cost for the registry coupling.

Alternatives considered

§
  • Keep isSettingsTemplate and add a parallel per-provider path. Rejected: two sources for "what is a settings template" is the drift ADR 0043 exists to prevent; the registry is the one source.
  • A JSON-object-level merge strategy on the provider. Rejected: it bakes in the JSON assumption the Codex TOML case breaks. A text-level strategy is the smallest seam that holds every future format.
  • Generate settings templates from the registry instead of seeding files. Rejected for the same reason ADR 0043 kept the .gitignore fragment authored: a settings template carries load-bearing, hand-tuned content (hook commands, permission denials); a dropped template guarded by the parity test is lighter and equally regression-proof.
choose openEsc close