Skip to content

InstallHooks: reconcile copied hooks against registered hooks after apply#1540

Closed
tzioup wants to merge 1 commit into
danielmiessler:mainfrom
tzioup:reconcile-hooks-at-install
Closed

InstallHooks: reconcile copied hooks against registered hooks after apply#1540
tzioup wants to merge 1 commit into
danielmiessler:mainfrom
tzioup:reconcile-hooks-at-install

Conversation

@tzioup

@tzioup tzioup commented Jul 19, 2026

Copy link
Copy Markdown

Fixes the detection gap in #1539.

What this changes

InstallHooks.ts does two independent things and verifies each on its own: it cpSyncs the payload hooks/ tree onto disk (checked by file count), and it merges hooks.json into settings.json (checked by added/skipped/events). Both succeed. Nothing compares the set of files copied against the set of hooks registered, so a hook can land on disk with no settings entry naming it and the install still reports success — accurately.

After --apply, this shells Doctor.ts --reconcile (already on disk at that point, copied by the same install) with CLAUDE_CONFIG_DIR pointed at the target config root, and includes its unwired list in the existing report JSON.

No logic is duplicated — the reconciler you shipped in v7.1.1 already does the transitive closure through dispatcher imports correctly. This just asks it.

Behaviour

On a clean v7.1.1 payload into an empty config root:

{
  "ok": true,
  "added": 45,
  "events": 11,
  "hookFilesCopied": 72,
  "unwired": [
    "LoopDetector.hook.ts",
    "DriftReminder.hook.ts",
    "PostToolObserver.hook.ts"
  ]
}

If Doctor.ts isn't present at that path, the report carries a reconcile string saying so rather than skipping silently — matching the "LOUD blocker, never a silent no-op" rule in Workflows/Setup.md.

Testing

Ran the patched installer against a scratch config root containing only settings.json ({}) and LIFEOS/TOOLS/Doctor.ts, with --skill-root pointed at this repo's LifeOS/. Output above is verbatim from that run. Existing report fields are unchanged; the two new keys are additive.

Note

unwired is reported, not enforced — this doesn't fail the install or change exit codes. Happy to make it a blocker instead, or to drop the subprocess and inline the comparison, whichever you'd prefer.

…pply

Copying the hook scripts and merging hooks.json are two independent
operations, each verified on its own. A hook can land on disk without any
settings entry naming it, and nothing currently compares the two sets.

After --apply, shell the shipped reconciler (Doctor.ts --reconcile, already
on disk at this point) and include its unwired list in the install report.
Degrades loudly if Doctor.ts is absent rather than skipping silently.

On a clean v7.1.1 payload this reports LoopDetector, DriftReminder and
PostToolObserver. Refs danielmiessler#1539.
@tzioup

tzioup commented Jul 21, 2026

Copy link
Copy Markdown
Author

A generalization this PR naturally extends to — recording it here rather than opening a separate issue, since the machinery is identical and already shipped.

Three pieces already exist; this PR connects the first two, for hooks. They generalize:

  1. Per-component provenance is already computed, then discarded. DeployComponents.ts builds a ComponentResult { component, actions[], applied, blockers[] } per component; InstallHooks.ts builds report { added, skipped, events, hookFiles }. Both are console.log'd and dropped — never persisted to disk.

  2. A durable, typed, tamper-evident manifest already exists. Doctor.ts writes LIFEOS/MEMORY/STATE/capabilities.json (interface Manifest, Doctor.ts:47/63/263). Today it records external-capability liveness (codex/interceptor/cloudflare/voice → live/broken/declined/stale), not deployment provenance.

  3. The reconciler this PR calls is hooks-only by designreconcileHooks() is explicitly annotated v1 scope: hooks only, two inventories (Doctor.ts:344).

Two low-cost generalizations, both building only on the above:

  • Persist the provenance. Fold the ComponentResult / report the deploy tools already compute into capabilities.json (or a sibling install-manifest.json) instead of discarding it. An install then carries a durable record of what was deployed, what was skipped, and why — which is what lets a later discrepancy be attributed (shipped-broken vs onboarding-declined vs onboarding-failed) instead of re-diagnosed from scratch on each machine.

  • Generalize the reconcile beyond hooks. This PR wires Doctor --reconcile after hook apply; the same declared-vs-deployed check applies to every component DeployComponents places (statusline, agents, commands, launchd, pulse). The hook slice is exactly what this PR already does — this is the same move applied to (a) all components and (b) a persisted record.

No new subsystem is required; all three pieces ship today.

Out of scope here, noted for completeness: the broader class of "entries removed from inside a surviving config/doc structure" (e.g. a registration deleted from an array that otherwise stays valid) is wider than the two-inventory reconcileHooks covers, and a declared-vs-registered reconcile won't catch all of it. Closing that class needs either configs generated-from-source (so a hand deletion regenerates or fails loudly) or a broader reconciler — a separate, larger design question, not something to fold into this PR.

@danielmiessler

Copy link
Copy Markdown
Owner

Ported into source, thank you. One note on mechanics: the local InstallHooks had diverged from the shipped copy (it already counts pre-existing files and skips collisions), so your diff was re-applied as intent onto that base rather than verbatim; behavior is exactly your design, reconcile after apply, unwired[] in the report, loud when Doctor.ts is absent. The provenance and generalize-beyond-hooks ideas from your follow-up comment are noted as separate design work. The public repo is generated at release, so this closes as ported with README credit. Ships with the next release.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants