ADR-169: agent-ways relinquishes user-scoped settings.json; retains only its operational baseline¶
Context¶
~/.claude/settings.json is Claude Code's declarative user configuration file. It
is also a shared-write object: several independent parties mutate it, and none of
them coordinates with the others.
- Claude Code owns it.
/configpersists user preferences (theme, model,autoCompactEnabled, tui/notification toggles) straight into~/.claude/settings.jsonat user scope, and the write target cannot be redirected. Claude Code additionally writes some keys in situ during a session (advisorModel,effortLevel— the "known in-situ writers" of ADR-147). - agent-ways writes two slices via
ways reconcile(tools/ways-cli/src/cmd/settings_merge.rs): the hook entries it ships, and its permission strings (WAYS_PERMSinpermissions.allow,WAYS_DENYinpermissions.deny, the ADR-152 secret-path baseline). This is a three-way merge keyed on a persisted last-applied base — the "one shared-write seam" of ADR-142. - agent-ways also ships a config-management service on top of that seam: the
composable fragment store (ADR-147) and the operator interview skill (ADR-149),
exposed as
ways settingsand theways-settingsskill. Its projector (settings/project.rs) is a second independent three-way writer that owns the fragment keys (model,env,statusLine, …) and deliberately skips hooks/permissions.
Three problems motivated this decision:
-
Over-reach. The fragment store makes agent-ways a general manager of user-scoped Claude Code configuration — config that is not agent-ways' concern.
~/.claude/is properly a dotfiles-class directory (the user's own machine config), and an application framework offering to own the whole of it is an overstep. ADR-163 already recorded the first symptom: agent-ways was force-claimingstatusLine, "exactly the anti-pattern ADR-147 set out to end," and named dotfiles the cross-host source of truth — but left the engine in agent-ways. -
A concrete redundancy bug.
WAYS_PERMSships bothEdit(~/.claude/**)andWrite(~/.claude/**), andWAYS_DENYshipsWrite(~/.ssh/**)alongside itsEdit/Readsiblings. Claude Code's file-permission checks are satisfied by theEdit(path)rule (which covers the file-writing tools), so theWrite(...)entries are inert and surface as a launch-time warning. Deleting them from the livesettings.jsondoes not stick: the reconciler re-adds them from the source constants on the nextways reconcile. -
Two things wanted to be true at once, and were assumed incompatible. The user should own their Claude config through dotfiles; and a fresh agent-ways install with no dotfiles must still be safe and usable. An early proposal to collapse to a single writer — lifting the baseline into the dotfiles store — was examined and rejected: a security baseline that only holds when dotfiles is deployed has a hole.
Two empirical facts (confirmed against code.claude.com/docs/settings and the
Claude Code precedence chain) constrain any solution:
- There is no user-scope
settings.local.json. The.local.jsonoverride exists only at project scope (.claude/settings.local.json). So there is no user-level side file into which a tool could isolate/config's writes; every user-scope writer lands in the same~/.claude/settings.json. - Because
/configand the in-situ writers mutate that single file, any tool that owns keys in it must use base-preservation (three-way) merge, not a naive compile-and-replace. A compiler that rewrites the file from fragments would clobber the operator's live/configtoggles on every deploy.
Finally, an explicit product constraint: the dotfiles-side config tool must work without agent-ways installed at all, and (symmetrically) agent-ways must work without dotfiles. Neither may depend on the other's binary.
Decision¶
agent-ways relinquishes management of user-scoped Claude Code configuration and
retains only its own operational and security baseline in settings.json.
- Retain the operational baseline, unchanged in mechanism. agent-ways keeps
settings_merge.rsas a three-way, base-preserving, self-auditing writer of exactly the slices it must own for the framework to function and to be safe standalone: hooks— the entries agent-ways ships (SessionStart disclosure, the ADR-162/167 deny backstop, etc.).permissions.allow— the operational allows for its own binaries (Bash(ways:*),Bash(attend:*),Bash(attend-chat:*),Bash(way-embed:*),Edit(~/.claude/**)).permissions.deny— the ADR-152 secret-path baseline (opt-out preserved).
This slice is disjoint from any user-preference key, self-audits (reverts on any change to an unmanaged field), and ships with the application so a fresh install is safe and usable with no dotfiles present.
-
Hand the user-config service to dotfiles; keep only self-management. What
ways settingsdid — author and project user-scoped configuration — did not work cleanly and conflicted with the premise that dotfiles owns the user's own config. That capability is handed to the dotfiles tool. agent-ways removes the fragment store and interview apparatus outright: theways settingsCLI subcommands (settings/project.rs,settings/compile.rs, and siblings), theways-settingsskill, and the now-orphaned schema plumbing (settings_schema_*inpaths.rs,settings_schema_urlinconfig.rs, therefresh-settings-schema.shscript, and the vendoredshare/claude-code-settings.schema.json). This supersedes ADR-147 and ADR-149. agent-ways keeps only enough configuration capability to manage itself — the baseline in (1), possibly nothing more; it retains no user-config surface. User-scoped configuration (model,statusLine,env, user-authored permissions, prefs) is no longer agent-ways' concern. -
User config moves to dotfiles. The dotfiles-side tool owns the fragment store and authoring experience and carries its own standalone three-way merger — it does not call the
waysbinary. Its architecture is recorded in a companion ADR in the dotfiles repository. agent-ways makes no claim on the keys that tool owns. -
Coexistence is by disjoint ownership, not a shared engine. Because each tool must run standalone, each carries its own three-way base-preserving merger over a disjoint set of owned keys, each keying on its own last-applied base and each self-auditing hands-off on keys it does not own. Multiple such writers over one
settings.jsonis safe precisely because their owned sets do not overlap — the model ADR-163 validated in practice. A single unified engine was rejected (see Alternatives): the standalone-independence requirement forecloses it. The exact invariant that makes independent writers safe — disjoint scalar/object ownership, additive-union on shared lists with each writer removing only what its own base recorded, per-writer last-applied base, and self-audit hands-off — is the peer-writer coexistence contract specified indocs/architecture/platform/ADR-500-settings-json-three-way-merge-spec-and-peer-writer-coexistence-contract.md. The dotfiles-side tool ports the same merge algorithm from that spec (shared design lineage, not a runtime dependency), so the two mergers behave identically without coupling. -
Fix the redundancy. Remove
Write(~/.claude/**)fromWAYS_PERMSandWrite(~/.ssh/**)fromWAYS_DENY.Edit(...)already covers the file-writing tools; theWrite(...)entries are inert and only produce the launch warning. Removing them at the source constants makes the fix durable through reconcile. -
No user-scope local-override layer. An earlier design sketch proposed a
settings.local.json"escape hatch" layer; it does not exist at user scope and is dropped. -
Authoritative key-set partition. Every user-scope
settings.jsonkey falls into exactly one of three buckets (mirrored in the dotfiles-side ADR-010 so the partition is agreed on both sides and disjointness holds by set-subtraction, not guesswork): -
(A) agent-ways baseline —
hooks(its shipped entries) +permissions.allow{Bash(ways:*), Bash(attend:*), Bash(attend-chat:*), Bash(way-embed:*), Edit(~/.claude/**)}+permissions.deny(WAYS_DENY). This is exactly theWAYS_PERMS/WAYS_DENYconstants — the constant is the boundary. - (B) user/dotfiles — everything else user-authored, including the
agent-ways-adjacent tooling that agent-ways does not ship as a core binary
(
way-match,kg,mmaid,adr/adr-tool, the knowledge-graph and thinking-strategies MCP servers, shell/prompt tooling, genericBash(...),Read(~/**), …). Owned by the operator via the dotfiles config tool. -
(C) Claude Code runtime — keys Claude Code writes autonomously (
modeland the/configtoggles;advisorModel,effortLevel, and other in-situ writes). Neither tool owns these; both must leave them to base-preservation. They must never be declared as a managed fragment, or the tool would thrash against Claude Code on every run. -
Relinquish protocol for the ownership handoff. The steady-state disjoint contract assumes ownership never moves. The retirement moves ownership of the user-fragment keys (
statusLine,attribution,env, …) from agent-ways' projector to the dotfiles tool, and that transition has a hazard the contract does not cover: if agent-ways' final act treated those keys as deprecated-ours (in its base, absent fromours), its three-way merge would remove them fromsettings.json— clobbering the value the dotfiles tool now asserts, order- dependently. The retirement therefore relinquishes rather than deprecated- removes: it clears agent-ways' fragment base for those keys and leaves the live values in place as foreign keys for the dotfiles tool to adopt. In practice, because the projector is removed entirely (not shipped in a deprecation mode), it simply never runs deprecated-removal again — removal is the relinquish, provided the removal performs no final "cleanup" projector pass. Adoption on the other side is the ordinary "migrating" behavior: a live foreign value is asserted asours, the base is seeded from it, and the result is idempotent. Once agent-ways stops touching the keys, steady-state disjointness is restored and run order no longer matters.
Ratification: the "how minimal" question — total removal of the user-config
service vs. keeping a minimal affordance for no-dotfiles operators — was ratified in
favour of self-management only: agent-ways keeps the baseline in (1) and no
user-config surface. An agent-ways-only operator uses the baseline plus raw
/config; managed user config is a dotfiles-tool adoption away. Retaining any
user-config service was rejected as reintroducing the over-reach this ADR removes. A
minimal user surface, if ever wanted, is a purely additive future change and does
not gate this decision.
Consequences¶
Positive¶
- agent-ways stops owning config that isn't its concern;
~/.claude/settings.jsonreturns to being the user's own file plus one narrow, auditable framework slice. - The launch-time permission warning is fixed durably.
- The security/operational baseline still travels with the app, so a standalone install is safe and usable with no dotfiles.
- Each tool is independently installable and testable; neither depends on the other.
- Removes a whole class of dual-control confusion: agent-ways no longer has two
writers into
settings.json(the fragment projector goes away).
Negative¶
- The three-way merge logic is duplicated across repositories (agent-ways keeps its baseline merger; dotfiles builds its own). This is the deliberate price of the standalone-independence constraint — one tested engine would have been less code but would have coupled the tools.
- Operators who used
ways settingsmust migrate their fragments to the dotfiles tool. A migration note is required. - Superseding two Accepted ADRs (147, 149) is a non-trivial reversal of recent design; the reasoning must be legible to anyone who read those first.
Neutral¶
- ADR-152's deny baseline is retained as-is, now framed explicitly as part of the operational baseline agent-ways keeps.
- ADR-163's "dotfiles is the source of truth" direction is carried to completion: the engine follows the store to dotfiles, rather than the store feeding an engine that stayed behind.
- ADR-142's projection model is unchanged except that the shared-write seam narrows to the baseline slice only.
Alternatives Considered¶
-
Single unified engine, layered fragment stores (L1 app-baseline / L2 user / L3 host / L4 local). One loader composing ordered fragments, borrowing the dotfiles
zshrcconf.dshape. Rejected on two grounds: (a) the standalone constraint requires each tool to writesettings.jsonwithout the other, so a single engine cannot live in only one repo; (b) the L4 user-scopesettings.local.jsonlayer it relied on does not exist in Claude Code. -
Lift the baseline (hooks +
WAYS_DENY) into the dotfiles store; one writer total. Rejected: a security baseline that only exists when dotfiles is deployed strands a fresh agent-ways install. The baseline must ship with the application. -
dotfiles depends on the
waysbinary as its merge engine (store-only dotfiles). The least-code option and initially preferred. Rejected by the explicit constraint that the dotfiles tool must work with no agent-ways installed. -
A naive compiler (compile-and-replace) instead of a three-way merger. Rejected:
/configand the in-situ writers mutate the same user-scope file, and a compiler would clobber the operator's live toggles on every deploy. Base preservation is mandatory. -
Keep
ways settingsas-is and merely re-posture it as opt-in. Rejected as insufficient: leaving the engine in agent-ways keeps the over-reach and the dual-writer surface the user objected to; ADR-163 already showed re-posturing alone does not stop the framework from claiming user keys.