Skip to content

MergeSettings drops every installer hook once settings.user.json defines any hook #2466

Open

Description

@mj-deving

Version

LifeOS 8.761.0 (tag v8.761.0, fb9aef4).

What is broken

settings.system.json deliberately ships without hooks; the installer writes them straight into settings.json, and mergeSettingsPreserving carries them forward on each regeneration. That carry-forward stops as soon as settings.user.json contains a hooks key at all. One user hook on one event, for example a single Stop command, replaces the whole installer hook set. SessionStart, LoadContext, SessionCleanup and the rest disappear from the regenerated settings.json, and no warning is printed. The code comment says a layer that defines the key wins. The result is that adding a single personal hook silently disables LifeOS's own hooks.

Where (file:line)

  • LIFEOS/TOOLS/MergeSettings.ts:162 lists hooks in INSTALLER_OWNED_KEYS.
  • LIFEOS/TOOLS/MergeSettings.ts:169-170: the existing value is copied only when neither layer defines the key, so any user hooks object wins whole.
  • LIFEOS/TOOLS/MergeSettings.ts:125: the only overlay guard in this path, guardPermissionOverlays, checks permissions, not hooks.

Repro on a clean tree

From the LifeOS/ directory of a v8.761.0 checkout:

bun -e 'import {mergeSettingsPreserving} from "./install/LIFEOS/TOOLS/MergeSettings.ts";
const existing = {hooks:{SessionStart:[{hooks:[{type:"command",command:"LoadContext.hook.ts"}]}]}};
const user = {hooks:{Stop:[{hooks:[{type:"command",command:"my-own.sh"}]}]}};
console.log(JSON.stringify(mergeSettingsPreserving({}, user, existing).hooks))'

Output:

{"Stop":[{"hooks":[{"type":"command","command":"my-own.sh"}]}]}

The SessionStart installer hook is gone.

Negative control

With an empty user layer ({} instead of user) the same call returns the installer hooks unchanged: {"SessionStart":[{"hooks":[{"type":"command","command":"LoadContext.hook.ts"}]}]}. Only the presence of a user hooks key drops them.

Suggested fix

Merge hooks per event instead of whole-object: keep the installer's entries from existing and append the user layer's entries for the same event, letting __remove delete a specific installer hook when that is wanted. At minimum, print a warning when a user hooks key shadows installer hooks that exist in the current settings.json. Untested.

Activity

  1. mj-deving commented on Oct 30, 2026

    @mj-deving
    Author

    Clarification on provenance: the repro above was first run on files byte-identical to the tag, not on a checkout. I have now re-run it on a fresh clone of v8.761.0 (fb9aef4, git status clean). Output is unchanged: with the user layer the result is {"Stop":[...my-own.sh...]} and SessionStart is gone; with an empty user layer the SessionStart hook is returned unchanged.

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