ADR-163: Config separation — dotfiles as source-of-truth feeding the settings fragment store¶
Context¶
ADR-147 built the user-scope settings fragment store ($XDG_CONFIG_HOME/agent-ways/settings/)
and its projector (ways settings project). It defined how fragments compile and
merge into ~/.claude/settings.json, but left two things open:
- Where fragments come from — the store is a per-host directory. Nothing said how an operator's config travels across their machines.
- File artifacts — ADR-147's Context named
statusline(and hook scripts) as file artifacts, but the built machinery projects only settings.json keys, never files.
The gap is not academic. Auditing one operator's two hosts (call them north and slab) surfaced silent drift that no CI catches:
statusline.shpresent on north, missing on slab — the settings pointer had propagated (legacy residue) but the script never did, so slab's status line was broken while itssettings.jsonstill referenced it.modeldiffered (opus[1m]vsopus); apermissions.denyguarding gh/docker credentials was present on north, absent on slab — a security drift.- Session-link (
Claude-Session:) suppression worked on north only because a per-host~/.claudememory told the agent to disobey the harness prompt — slab lacked the memory and leaked the URL into commits/PRs. (This ADR originally recorded ADR-162's reason: thatattribution.sessionUrlwas broken upstream, leaving a mechanical deny hook as the real fix. That premise was false — ADR-167 proves the key governs the link and supersedes ADR-162. The observation above is unaffected: the control, whatever it is, only defends the hosts it reaches. Distributing it is this ADR's concern, and the drift is the point.)
Separately, the framework repo was force-claiming a user-scoped key: statusLine sat
inert in the repo-tracked settings.json (reconcile co-owns only hooks +
permissions, so it was never projected) — exactly the anti-pattern ADR-147 set out to
end.
Decision¶
dotfiles is the operator's cross-host source-of-truth; agent-ways compiles and projects what dotfiles feeds it. The layering:
dotfiles (VCS, per-operator)
├─ settings fragments ──deploy──► $XDG_CONFIG_HOME/agent-ways/settings/ (ADR-147 store)
│ └─ ways settings project ──► ~/.claude/settings.json (KEYS)
└─ file artifacts (statusline.sh, …) ──deploy──► ~/.claude/ (FILES)
Four commitments:
-
Artifact-ownership split. The fragment store owns settings.json keys; dotfiles owns file artifacts and deploys them directly to
~/.claude. One owner per artifact — no file has two writers. (statusLinethe key → fragment;statusline.shthe file → dotfiles.) -
The framework de-claims user-scoped keys. The repo-tracked
settings.jsonships only whatways reconcileco-owns (hooks+permissions).statusLinewas removed (PR #347, merged). -
The fragment store is the projection boundary. dotfiles never writes
~/.claude'ssettings.jsondirectly — it deploys the store, andways settings projectperforms the three-way merge. agent-ways stays the singlesettings.jsonwriter (alongside reconcile's two co-owned slices), so the operator's own keys survive. -
Projector-base hygiene is part of the contract. The projector's per-host last-applied base (
$XDG_STATE/agent-ways/settings-fragments-<scope>.json) is host-local ephemeral state, not config. It can carry ghosts — keys a prior projection managed but the store no longer declares — which cause surprising cross-host retractions. (Observed: a stale base recordingmodel: opuswould, if carried to another host, silently retract that host's model to the default.) The base is reset when the store's ownership set changes; dotfiles never deploys it.
Consequences¶
- Cross-host config becomes reproducible: clone dotfiles → deploy →
ways settings project, and any host converges to a coherent Claude Code config. - Session-link suppression (ADR-162) stops being a per-host memory hack: the deny hook becomes a primitive distributed through this same pipeline.
- The two disjoint settings writers (
ways reconcilevsways settings project) that ADR-147 left unreconciled remain disjoint here; unifying them is out of scope (follow-up). - File-artifact projection stays outside agent-ways (owned by dotfiles). If the file set grows, revisit building projection into the fragment store (see Alternatives).
Alternatives Considered¶
- Plain dotfiles writes
~/.claude/settings.jsondirectly. Rejected: two owners (dotfiles +ways settings project/reconcile) fighting one file — the exact drift that produced the slab breakage. - Personal config lives in the framework repo. Rejected: revives ADR-147's
force-claiming anti-pattern (the inert
statusLineis the cautionary case). - Build file-artifact projection into agent-ways so it owns
statusline.shtoo. Deferred, not rejected: cleaner single-owner end-state, but net-new machinery. The dotfiles-owns-files split ships today and proves the loop; revisit when the file set justifies it.
Validating implementation (the statusline pilot)¶
statusLine was the pilot that exercised every seam:
- De-claimed from the repo (
statusLineremoved from trackedsettings.json, PR #347, merged). - Authored as a user-scope fragment (
10-statusLine.md) in the store. - Projected cleanly — the stale projector base was reset first to clear ghosts, after
which
ways settings projectreportedalready up to date. - Distributed — both the fragment store and
statusline.share now dotfiles-managed symlink entries (claude-settings,claude-statusline), committed and pushed, so a second host reproduces the config withdotfiles pull && dotfiles deploy && ways settings project.
Attribution/session-link suppression (ADR-162) is the second, higher-value pass over this same pipeline.