# Crash-safe ownership journal for installer-managed settings entries

How an installer adding a fixed set of entries, such as exact permission allow rules, to a developer-edited settings file can install, repair and uninstall them without deleting developer entries, overriding denies or erasing unrelated edits, how recovery decides a write landed from an unforgeable witness, and when an absent entry is evidence at all.

Exact reference: {"kind":"skill_version","skill_id":"skl_kpzAm8lR5wTfU3kKBrZGgA","version_id":"skv_F2Vk3OLl1DFQ3xCGlKx3TA"}

Applicability: [{"constraint":"Entries are identifiable by exact value or a canonical form, and occurrences can be counted","technology":"JSON settings files edited by both developers and tools","version_scheme":"unknown"},{"constraint":"Atomic rename within one directory, stable file identity readable before and after the rename, and on some platforms a stronger full-flush call for durability","technology":"POSIX filesystems","version_scheme":"unknown"},{"constraint":"Recovery runs only at the next invocation, which may be much later, and the host may rewrite the file meanwhile","technology":"Installers with no background process","version_scheme":"unknown"}]

# Crash-safe ownership journal for installer-managed settings entries

## When to use
Use this when a command-line installer, plugin or setup script adds a fixed managed set of entries, such as exact-string tool permission allow rules, to a settings file the developer also edits by hand, and must later repair or uninstall them. It applies when the host application may reformat the same file, when recovery can happen only at the next invocation and possibly much later, and when deleting an entry the developer authored would be unacceptable.

## Failures it prevents
- Uninstall deletes an entry the developer authored, because ownership was inferred from file content.
- Recovery decides a write landed from file content, and so claims an entry the developer or the host added inside the crash window. This is the same defect as inferring ownership from content, one step removed.
- A landed verdict computed over an empty set of effects is vacuously true, declaring a write landed with no evidence at all.
- A mistaken not-landed verdict rolls ownership back, then re-classifies the installer's own still-present entries as developer-authored, leaking every grant permanently.
- A single mis-parse, or a host migration that moves the container, makes every owned entry look absent at once, and one unconfirmed observation durably lapses all of them.
- The command the developer actually runs to fix things has no defined answer for a lapsed entry, so it becomes either a permanent no-op or a silent restoration of a revoked grant.
- One acknowledgement covers a batch of re-grants, so the entry the developer deliberately revoked is restored alongside the ones the host lost.
- Repair silently restores a grant the developer deliberately deleted.
- Uninstall restores a pre-install snapshot and erases unrelated later edits.
- A re-install demotes owned entries to developer entries, so their grants can never be removed.
- An older installer build retires entries a newer build owns, and the two flap on every alternation.
- A duplicate copy of a managed entry permanently withdraws the authority to remove any copy.
- An ambiguous verdict becomes absorbing, so the product can never be cleanly uninstalled.
- Two concurrent runs of the same installer overwrite each other's write-ahead record.
- A post-write check confirms that untouched keys are unchanged, but skips the one key the write touched, which is where damage happens.

## Steps

1. **Two artifacts, two lifetimes.** Keep a durable ownership journal holding classification, and a separate transient pending record describing one in-flight change. The journal is deleted only once an uninstall has completed and every entry is terminal. The pending record is resolved at the end of every command, including ambiguous ones. Deleting the journal at the end of a normal install leaks every grant.

2. **Ownership is historical, never observational.** File content may revoke ownership; it may never establish it. With no journal, matching entries are left in place and reported, never adopted. Deciding whether a write landed is an ownership judgement, so it obeys the same rule: it may not rest on content alone.

3. **Record per effect** the container key path addressed by key rather than array position, the exact value, the occurrence count observed at plan time, the count expected after the write, a classification, and the generation of the managed set that introduced it.

4. **Classify only entries the journal does not already know.** Already owned stays owned, so a re-install cannot demote its own entries. A value already present becomes preexisting and is never removed. A value an explicit deny covers is blocked and never added. A value now present only in a narrower or altered form is conflicted, not added, and reported. States that record a reason rather than a history, namely blocked, lapsed and withdrawn, are re-evaluated against the current file on every run under steps 11 and 13; only owned and preexisting are exempt from re-evaluation.

5. **Take an exclusive lock for the whole command.** Two runs of the same installer are cooperating writers, so a lock genuinely protects against them even though it cannot bind the developer's editor or the host. If locking is unavailable, name pending records per invocation and have recovery iterate over all of them, rather than keeping one slot that a second run silently destroys.

6. **Write-ahead ordering, with a witness.** Build the settings temp file first and read its file identity, which the rename preserves. Persist a pending record holding the planned effects with both counts, the conflicts deliberately kept and the reason for each, the base content hash, the base file identity, the identity the temp file will carry after the rename, and the resolved target. Make the record durable with the same flush primitive used for settings. Then re-check the base hash and identity, rename, flush the directory, and only then mark the record committed. If any path between the record becoming durable and the rename exits without renaming, durably mark the record aborted; that knowledge is definitive and must not be thrown away.

7. **Patch, never restore.** Change only owned effects in the live document. Confirm first that parsing and re-serializing the unmodified bytes reproduces them; otherwise edit the text surgically. Before the rename, re-parse the bytes produced and assert both that every untouched key is unchanged and that each touched collection equals its prior contents minus exactly the planned occurrences of exactly the planned values. Preserve file mode, and for a linked path write beside the resolved target on the same filesystem, since a cross-device fallback would void atomicity.

8. **One removal procedure, shared by every command including recovery.** Remove an owned effect only when its recorded value is present at its recorded count. Absent means do nothing, and it may be recorded as lapsed only under the clean-read precondition in step 11. Present at a count above the recorded one means remove exactly the recorded number of occurrences: identical values are interchangeable, so counting alone guarantees no developer copy is lost. Present at a count below the recorded one, or only as a near match, means remove nothing and record a conflict. Near matching may only report and block additions; it must never reach the removal path. An owned entry a deny now covers is removed through this same procedure, and denies are never added, edited, reordered or removed.

9. **Deciding whether the write landed.** This is one bit per rename, and it must rest on a witness other writers cannot forge. Compare the file identity now at the target against the two identities recorded before the rename: the post-rename identity means landed, the base identity means not landed, and both verdicts hold regardless of what the entries look like. Check identity before counts, because the rename necessarily changes identity, so an unchanged identity is conclusive on its own. Only when identity is inconclusive, because the host replaced the file, may content corroborate, and then only under two preconditions: at least two effects that are not no-ops, and unanimous agreement among them. One effect cannot certify itself, since its presence and its absence are each consistent with two opposite histories. An empty set of such effects certifies nothing either; treat it as a precondition failure rather than a satisfied universal. Everything else is undecided.

10. **Recovery actions.** Landed means finish bookkeeping only, using the effects the pending record already holds, and an entry planned for removal that is still present was re-created by the developer, so report it and leave it. Not landed, on positive evidence, means roll the journal back; any value the rolled-back plan intended to add that is nonetheless present becomes unverified, never silently preexisting, because reclassifying it as developer-authored leaks the grant forever. Undecided means change nothing in settings, but still resolve the pending record by transcribing its effects into the journal as unverified and clearing the slot: ambiguity is durable state, a write-ahead slot is not. Unverified entries need an explicit adopt before they can ever be removed, and uninstall may finish while leaving them in place, marking them terminal and exiting with a status distinct from success. Read-only commands write nothing and only report that recovery is pending, and recovery never completes an uninstall during an unrelated command.

11. **Terminal is not always permanent, and absence is not always evidence.** Distinguish terminal by completion, where an uninstall finished, from withdrawn, where an entry was removed for a reason that may later stop holding. An entry withdrawn because a deny covered it, or left behind by an abandoned uninstall, becomes eligible again for ordinary classification once that reason is gone; otherwise the installer is silently and permanently incomplete. A conflicted entry is retained while the variant that caused the conflict is present, and is re-reported at every later invocation.

    An absent owned value is evidence of lapse only from a clean read: the file parsed without falling back, the recorded container resolved, and at least one other managed key was found where expected. A container that does not resolve is never evidence of lapse. A host migration to a new key path, a reinitialisation to defaults, and a dialect the host accepts but the parser does not all present as a missing container, and each lapses every owned entry in one pass. Record the first qualifying observation as a provisional lapse stamped with the file identity it was seen at, and promote it to settled only on a second qualifying observation at a different identity. A provisional lapse is resurrectable in the same way as a withdrawal; a settled one still yields to an explicit adopt.

12. **Generation comparison is ordered.** Retire an owned entry only when its recorded generation is strictly older than the running installer's and the entry is absent from the current managed set. A recorded generation that is newer or unrecognised means leave the entry alone and report that a newer build manages it. Keying retirement on absence from the current set alone lets an older build delete a newer build's grants.

13. **Repair and install do not guess at intent, and they answer per entry.** An absent owned value means the same thing in every command: lapsed. Deliberate deletion is the most common way a developer revokes a grant and it leaves no residue, so absence of evidence of intent is not evidence of accident. Record with each lapse the cause actually known: removed by this installer's own uninstall, lost while the host demonstrably rewrote the file, or unexplained. Install and repair both re-add a lapse of the first two kinds and both require an explicit request for an unexplained one, and neither re-adds a value a deny now covers. Leaving install undefined here is not neutral: refusing every lapse makes install a permanent no-op after one accidental deletion, while accepting every lapse makes the request pointless, because install is the command developers run. Make the request per entry and name the exact value being restored, since one acknowledgement covering a batch restores the grant the developer revoked alongside the ones the host lost.

## Limits
- A lock binds only other runs of the same installer. The race with the developer's editor and with the host is narrowed by re-checking base hash and identity immediately before the rename, but a residual window is inherent to rename and cannot be closed.
- File identity as a witness assumes it is stable and not promptly reused. Where it is not, a per-write random value held in the durable journal and written into the file is the portable fallback, but a host that strips unknown keys defeats it; detect that once, record it, and degrade to requiring an explicit adopt rather than silently returning to content-only inference.
- An in-file monotonic counter is not a valid witness: it restarts after a strip and is reproduced by a restored backup or a copied settings file.
- The undecided branch leaves grants in place, which is safe for developer data and unsafe for authority. Its report must be prominent and repeated, and the opposite trade needs an explicit override.
- Lapse confirmation costs a second invocation, so a genuine deletion is reported later than it happened. That delay is the price of not lapsing everything on one bad read.
- A developer who deletes an owned entry and retypes the identical value before any command runs leaves no trace, so a later uninstall removes their copy. This is inherent to identifying entries by exact value, not a defect the journal can repair.
- A host that canonicalises entries can turn an owned value into an apparent developer edit and orphan the grant. Recording a canonical form alongside the exact value reduces this but does not remove it.
- Ownership across moved or version-controlled settings files needs separate design.

## Evidence status
Reasoned design only, refined across four independent reviews that were themselves carried out by reasoning. No implementation exists, and no tests or crash-injection runs have been executed. The witness mechanism in step 9 is the least settled part and should be treated as unverified.

## Suggested tests
- Crash injection at each step of install, repair and uninstall, with plans of zero, one, two and the full number of effects that are not no-ops.
- An install whose write did not land, followed by the developer or the host adding one managed value by hand before the next invocation.
- An uninstall whose write landed but was never committed, followed by the developer re-creating the values by hand.
- A forced mistaken not-landed verdict, checking that still-present intended additions become unverified rather than preexisting.
- A host migration that moves the permissions container, and separately a dialect the parser rejects, each observed by a read-only command, checking that no lapse is recorded and that the next clean read restores normal operation.
- An accidental loss of every managed entry followed by install and then repair, checking that an entry the developer deliberately deleted is named and withheld separately from the rest.
- A developer deleting one managed entry outright, then running repair.
- A deny added after install and later removed, checked at repair and uninstall.
- An older installer build run against a journal written by a newer one.
- Two installer runs started concurrently against the same file.
- A duplicate copy added after install, then uninstall.
- A host-driven reformat, a host that strips unknown keys, and a settings file copied from another machine.

## Supporting basis and limitations

Reasoned analysis only. Nothing was implemented and no tests or crash-injection runs were executed, so the procedure remains unverified in practice. The design was re-derived in the cited conversation and then handed to an independent reviewer that reasoned only, read no files and ran no commands.

Most of that reviewer's findings were already covered by the version being updated, which is useful signal in itself and is why this change is narrow. Already covered: re-checking the base hash and file identity immediately before the rename, so a developer edit between read and rename cannot be clobbered silently; retaining the plan in the durable pending record so a landed but uncommitted install can finish its bookkeeping instead of reclassifying its own entries as developer-authored; making an entry withdrawn under a covering deny eligible again once the deny is gone; read-only commands writing nothing, which removes the race on the journal; and requiring an older build to report rather than silently skip entries a newer generation manages.

Two findings survived and are the substance of this change.

First, the lapse rule was unconditional. The load-bearing sequence: the installer owns every managed entry; the host then rewrites settings, moving the permissions container to a new key path, reinitialising to defaults, or emitting a dialect the host accepts and the installer's parser does not; the developer runs any command, including a read-only one; absence fires for every entry at once and is recorded durably. The host migration then completes with all entries still present and still granted, but nothing owns them, uninstall skips them, and re-adoption is forbidden. A container that does not resolve is far more likely to mean host churn or a parse mismatch than a developer deleting every entry individually, so it is the wrong default, and the provisional-then-confirm rule at a second file identity separates the two.

Second, install had no rule for a lapsed entry while repair did. Refusing every lapse makes install a permanent no-op after one accidental deletion; accepting every lapse makes the acknowledgement pointless. Recording the cause is what lets the two commands agree.

A related narrowing: the existing tombstone reasoning protects against a deliberate deletion the installer observed, but a developer who deletes an entry and retypes the identical value with no command running in between leaves no trace, and a later uninstall removes their copy. That is inherent to exact-value identity rather than a defect in the rule, and is now stated rather than implied.

One reviewer claim was not adopted. It argued the below-count branch of the removal procedure is unreachable because the installer always writes one copy. That holds for exact-string entries but not for the canonical-form case the applicability already admits, so the branch stays.

## Change and rationale

Makes an absent owned value evidence of lapse only from a clean read, so a host migration or a parser mismatch that hides the container can no longer lapse every managed entry at once; a first observation is provisional and must be confirmed at a second file identity. Adds a recorded cause to each lapse and gives install, not only repair, a defined answer for a lapsed entry, since install is the command developers actually run. Requires the re-grant acknowledgement to be per entry and to name the exact value, so one batch approval cannot quietly restore a grant the developer deliberately revoked.

The previous version closed the landing-verdict and ownership-inference holes but still let a single observation destroy ownership. Absence was read as lapse unconditionally, and a container that failed to resolve counted as absence, so one host migration, reinitialisation to defaults or parser mismatch durably lapsed every managed entry, which the invariant that content may never establish ownership then makes unrecoverable. Install also had no stated behaviour for a lapsed entry while repair did, so implementations diverge into either a permanent no-op or a silent re-grant that defeats the repair gate. Both outcomes are a grant leak and a silent re-grant, the two things the pattern exists to prevent.
