danielmiessler / danielmiessler/LifeOS
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
- Dominant language
- TypeScript
- Stars
- 19k
- Forks
- 2.5k
- Avg merge
- 8d 17h
- Merged PRs (30d)
- 1
Description
## 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 `/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 `/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
```shell
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:
```shell
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
- [x] I searched open and closed issues for this defect.
Searched `settings.system.json MergeSettings`, `settings split merge fresh install`. Prior
art exists and this is scoped around it: **#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 #1432** proposed writing a standing
`settings.system.json` on hooks install and was **closed unmerged**; **#1575** (closed
COMPLETED) covers the `MergeSettings` user-overlay path footgun; **#1860** (closed COMPLETED)
covers `settings.system.json` hardening never reaching updated installs. All four concern the
install and merge behaviour. None of them changes `ConfigSystem.md`, and none documents the
authority flip. This report is the docs-side contradiction those four left open.
- [x] The repro runs against a clean tree of the version above, not against my modified install.
Fresh `--depth 1` clone; `DeployCore` and `InstallSettings` run with `--config-root` and
`HOME` pinned to a scratch directory, so nothing resolves to a real install.
- [x] I removed personal data from the pasted output — real names, absolute home paths, tokens, my
own content. Paths are `/tmp/...`; `PROBE_MARKER` is synthetic.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with ConfigSystem.md:39,66-70, then compare its claims with InstallSettings.ts:71-72, hooks.json:456, and MergeSettings.ts:601-607. Run the clean-tree reproduction to confirm the stock install and hand-established split differ. Done means the documentation distinguishes both states and clearly explains the authority change and its overwrite risk.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- documentation
- Issue type
- Documentation
- Difficulty
- 2/5
- Estimated time
- Half a day
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100