Version
LifeOS 7.40.4 / Config
What is broken
ConfigSystem.md:39 describes settings.json as SYSTEM (generated) — "Merged at SessionStart by
MergeSettings.ts from settings.system.json + settings.user.json. Read-only at runtime —
manual edits get overwritten next session." :70 makes it an instruction: "NEVER edit
settings.json directly — it's generated. Edits get overwritten on next SessionStart." Neither
sentence is conditional, and :66-67 sends the operator to settings.user.json and
settings.system.json for the two classes of change.
On a stock install neither of those two source files exists, and by design: InstallSettings.ts
writes the payload template straight to <configRoot>/settings.json, and MergeSettings.ts:601-607
no-ops on the missing inputs under a comment naming the behaviour a deliberate fresh-install guard.
So on that install the documentation is false in both directions at once:
- the two files
:66-67 tells the operator to edit do not exist;
settings.json is not regenerated on any session, so "manual edits get overwritten next
session" is wrong — a hand edit to settings.json is in fact the only edit that takes effect,
and it survives indefinitely;
- the one file
:70 forbids editing is therefore the only place a change can be made.
The second half is the sharper one, and it is undocumented in either state. An operator who follows
:66-67 and hand-creates settings.system.json silently changes which file is authoritative.
The merge starts firing that session, settings.json becomes genuinely generated, and any edits
they had made to it — the edits the install left them no alternative to — are overwritten with no
warning. Nothing errors, both messages are calm, and both exits are 0. The negative control below
demonstrates that flip on the unpatched tag.
Reader and writer:
- contract —
LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:39 — "Merged at SessionStart by MergeSettings.ts … Read-only at runtime — manual edits get overwritten next session"
- contract —
LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:66-67 — "edit LIFEOS/USER/CONFIG/settings.user.json" / "edit settings.system.json"
- contract —
LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:70 — "NEVER edit settings.json directly — it's generated."
- writer —
LifeOS/Tools/InstallSettings.ts:71-72 — template install/settings.system.json → target <configRoot>/settings.json; no settings.system.json is written at the config root
- reader —
LifeOS/install/hooks/hooks.json:456 — SessionStart runs SettingsBackport.ts then MergeSettings.ts --system … --user … --output …
- the deliberate no-op —
LifeOS/install/LIFEOS/TOOLS/MergeSettings.ts:601-607 — "Fresh-install guard (audit 20260702 F-001)"
Where (file:line)
LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:39
Repro on a clean tree
git clone --branch v7.40.4 --depth 1 https://github.com/danielmiessler/LifeOS.git /tmp/lifeos-7404
cd /tmp/lifeos-7404 && git rev-parse HEAD
# → be9e8ef889f00a29f4fd677dee4772fdf32e07ce
mkdir -p /tmp/fresh/.claude /tmp/fresh/.config/LIFEOS
# Deploy the runtime, then the settings, exactly as setup does.
env HOME=/tmp/fresh CLAUDE_CONFIG_DIR=/tmp/fresh/.claude \
bun LifeOS/Tools/DeployCore.ts --config-root /tmp/fresh/.claude --skill-root ./LifeOS --apply >/dev/null
env HOME=/tmp/fresh CLAUDE_CONFIG_DIR=/tmp/fresh/.claude \
bun LifeOS/Tools/InstallSettings.ts --config-root /tmp/fresh/.claude --skill-root ./LifeOS --apply
# → { "ok": true, "apply": true, "target": "/tmp/fresh/.claude/settings.json",
# → "envValuesExpanded": 3, "mode": "create", "topLevelKeys": 28 }
# Neither file ConfigSystem.md:66-67 tells the operator to edit was created.
ls -1 /tmp/fresh/.claude | grep -i settings
echo "settings.system.json present? $([ -f /tmp/fresh/.claude/settings.system.json ] && echo YES || echo NO)"
echo "USER/CONFIG/settings.user.json present? $([ -f /tmp/fresh/.claude/LIFEOS/USER/CONFIG/settings.user.json ] && echo YES || echo NO)"
# → settings.json
# → settings.system.json present? NO
# → USER/CONFIG/settings.user.json present? NO
# Now the SessionStart command, taken verbatim from install/hooks/hooks.json:456.
before=$(shasum -a 256 /tmp/fresh/.claude/settings.json | cut -d' ' -f1)
env HOME=/tmp/fresh bash -c 'bun $HOME/.claude/LIFEOS/TOOLS/SettingsBackport.ts; bun $HOME/.claude/LIFEOS/TOOLS/MergeSettings.ts --system $HOME/.claude/settings.system.json --user $HOME/.claude/LIFEOS/USER/CONFIG/settings.user.json --output $HOME/.claude/settings.json'
after=$(shasum -a 256 /tmp/fresh/.claude/settings.json | cut -d' ' -f1)
echo "changed? $([ "$before" = "$after" ] && echo NO || echo YES)"
# → settings-layer not present (no settings.system.json / settings.user.json) — nothing to backport
# → MergeSettings: input(s) absent (system/user settings layer not established) — nothing to merge
# → changed? NO
# Byte-identical. Repeat the session as many times as you like; the answer does not
# change, so "manual edits get overwritten next session" never becomes true.
Negative control
The claim is about the docs and the transition, not about MergeSettings being broken, so the
control has to show the machinery working. Establish the split by hand — which is exactly what
ConfigSystem.md:66-67 tells the operator to do — and run the identical SessionStart command:
cp /tmp/fresh/.claude/settings.json /tmp/fresh/.claude/settings.system.json
mkdir -p /tmp/fresh/.claude/LIFEOS/USER/CONFIG
printf '{"env":{"PROBE_MARKER":"control-ran"}}\n' > /tmp/fresh/.claude/LIFEOS/USER/CONFIG/settings.user.json
before=$(shasum -a 256 /tmp/fresh/.claude/settings.json | cut -d' ' -f1)
env HOME=/tmp/fresh bash -c 'bun $HOME/.claude/LIFEOS/TOOLS/SettingsBackport.ts; bun $HOME/.claude/LIFEOS/TOOLS/MergeSettings.ts --system $HOME/.claude/settings.system.json --user $HOME/.claude/LIFEOS/USER/CONFIG/settings.user.json --output $HOME/.claude/settings.json'
after=$(shasum -a 256 /tmp/fresh/.claude/settings.json | cut -d' ' -f1)
echo "changed? $([ "$before" = "$after" ] && echo NO || echo YES)"
grep -c PROBE_MARKER /tmp/fresh/.claude/settings.json
no merge snapshot at /tmp/fresh/.claude/LIFEOS/MEMORY/STATE/settings-merge-snapshot.json — skipping backport (next MergeSettings run writes it)
Merged 28 system keys + 1 user overlays into /tmp/fresh/.claude/settings.json
changed? YES
1
Red twice over. First, everything ConfigSystem.md describes works — it is only ever reached on an
install where somebody established the split by hand, which the installer deliberately does not do.
Second, and this is the undocumented part: one cp moved authority from settings.json to
settings.system.json, settings.json was rewritten in the same session, and no message said so.
On a real install the file overwritten there is the one holding the operator's own edits.
Suggested fix
Shape only, untested. This is a docs change in the first instance, since the install behaviour
is settled:
- State the condition. Make
:39 and :70 conditional on the split existing. A stock install
has one file, it is not generated, and editing it is the supported path — say that, rather than
forbidding the only editable file.
- Document the transition. Add the step that establishes the split, and say plainly that
taking it makes settings.json generated from that session on and overwrites what is in it.
That is the sentence that does not exist in either state today.
- Optionally, have
MergeSettings's fresh-install message name the transition — it already
detects the exact condition, and it is the only line an operator sees.
The alternative — having InstallSettings.ts establish the split so the docs become true as
written — is PR #1432's direction and was closed unmerged, so I am not re-proposing it.
Before submitting
Version
LifeOS 7.40.4 / Config
What is broken
ConfigSystem.md:39describessettings.jsonasSYSTEM (generated)— "Merged at SessionStart byMergeSettings.tsfromsettings.system.json+settings.user.json. Read-only at runtime —manual edits get overwritten next session."
:70makes it an instruction: "NEVER editsettings.jsondirectly — it's generated. Edits get overwritten on next SessionStart." Neithersentence is conditional, and
:66-67sends the operator tosettings.user.jsonandsettings.system.jsonfor the two classes of change.On a stock install neither of those two source files exists, and by design:
InstallSettings.tswrites the payload template straight to
<configRoot>/settings.json, andMergeSettings.ts:601-607no-ops on the missing inputs under a comment naming the behaviour a deliberate fresh-install guard.
So on that install the documentation is false in both directions at once:
:66-67tells the operator to edit do not exist;settings.jsonis not regenerated on any session, so "manual edits get overwritten nextsession" is wrong — a hand edit to
settings.jsonis in fact the only edit that takes effect,and it survives indefinitely;
:70forbids editing is therefore the only place a change can be made.The second half is the sharper one, and it is undocumented in either state. An operator who follows
:66-67and hand-createssettings.system.jsonsilently changes which file is authoritative.The merge starts firing that session,
settings.jsonbecomes genuinely generated, and any editsthey had made to it — the edits the install left them no alternative to — are overwritten with no
warning. Nothing errors, both messages are calm, and both exits are 0. The negative control below
demonstrates that flip on the unpatched tag.
Reader and writer:
LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:39— "Merged at SessionStart byMergeSettings.ts… Read-only at runtime — manual edits get overwritten next session"LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:66-67— "editLIFEOS/USER/CONFIG/settings.user.json" / "editsettings.system.json"LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:70— "NEVER editsettings.jsondirectly — it's generated."LifeOS/Tools/InstallSettings.ts:71-72— templateinstall/settings.system.json→ target<configRoot>/settings.json; nosettings.system.jsonis written at the config rootLifeOS/install/hooks/hooks.json:456— SessionStart runsSettingsBackport.tsthenMergeSettings.ts --system … --user … --output …LifeOS/install/LIFEOS/TOOLS/MergeSettings.ts:601-607— "Fresh-install guard (audit 20260702 F-001)"Where (file:line)
LifeOS/install/LIFEOS/DOCUMENTATION/Config/ConfigSystem.md:39Repro on a clean tree
Negative control
The claim is about the docs and the transition, not about
MergeSettingsbeing broken, so thecontrol has to show the machinery working. Establish the split by hand — which is exactly what
ConfigSystem.md:66-67tells the operator to do — and run the identical SessionStart command:Red twice over. First, everything
ConfigSystem.mddescribes works — it is only ever reached on aninstall where somebody established the split by hand, which the installer deliberately does not do.
Second, and this is the undocumented part: one
cpmoved authority fromsettings.jsontosettings.system.json,settings.jsonwas rewritten in the same session, and no message said so.On a real install the file overwritten there is the one holding the operator's own edits.
Suggested fix
Shape only, untested. This is a docs change in the first instance, since the install behaviour
is settled:
:39and:70conditional on the split existing. A stock installhas one file, it is not generated, and editing it is the supported path — say that, rather than
forbidding the only editable file.
taking it makes
settings.jsongenerated from that session on and overwrites what is in it.That is the sentence that does not exist in either state today.
MergeSettings's fresh-install message name the transition — it alreadydetects the exact condition, and it is the only line an operator sees.
The alternative — having
InstallSettings.tsestablish the split so the docs become true aswritten — is PR #1432's direction and was closed unmerged, so I am not re-proposing it.
Before submitting
Searched
settings.system.json MergeSettings,settings split merge fresh install. Priorart exists and this is scoped around it: 6.0.0: settings.system.json contract mismatch — payload ships it without hooks, MemoryHealthCheck requires hooks registered in it (fresh installs report CRITICAL) #1402 (closed COMPLETED) states in its body that
Setup overlays the payload as
settings.json"so nothing ever lands at~/.claude/settings.system.json"; PR fix(install): write standing settings.system.json snapshot on hooks install (#1402) #1432 proposed writing a standingsettings.system.jsonon hooks install and was closed unmerged; MergeSettings: root-level settings.user.json is silently ignored (user-overlay path footgun) #1575 (closedCOMPLETED) covers the
MergeSettingsuser-overlay path footgun; settings.system.json hardening never reaches updated installs: the update path merges hooks only #1860 (closed COMPLETED)covers
settings.system.jsonhardening never reaching updated installs. All four concern theinstall and merge behaviour. None of them changes
ConfigSystem.md, and none documents theauthority flip. This report is the docs-side contradiction those four left open.
Fresh
--depth 1clone;DeployCoreandInstallSettingsrun with--config-rootandHOMEpinned to a scratch directory, so nothing resolves to a real install.own content. Paths are
/tmp/...;PROBE_MARKERis synthetic.