Skip to content

ConfigSystem.md documents settings.json as generated and never-edit while the shipped install deliberately never establishes the split; hand-creating settings.system.json silently flips which file is authoritative #2086

Description

@Steffen025

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:

  1. 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.
  2. 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.
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions