# 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, and how an uninstall spanning several containers stays recoverable.

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

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"},{"constraint":"The managed set may be split across more than one file or container, so one command performs more than one atomic replacement","technology":"Settings that span several files or containers","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, when the managed set may be split across more than one file or container, 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.
- An uninstall spanning several containers is interrupted between them, and recovery either replays a plan built before the developer edited a later container or abandons the remaining containers and leaks their grants.
- 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 probe concludes that the host preserves unknown keys because it was read back before the host had rewritten the file, and that cached conclusion later converts an undecided verdict into a verdict that authorizes writing.
- 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 in every container 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, once per container.** 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. Denies are never added, edited, reordered or removed. An owned entry withdrawn because a deny now covers it is withdrawn through this same procedure, and coverage is computed by exact value only. If the deny language admits patterns, a pattern that appears to cover an owned value is an inference, so it may report and block an addition but may never authorise a removal, for the same reason near matching may not.

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.

14. **One command, several containers: each container is its own commit unit.** When the managed set spans more than one file or container, the command performs more than one atomic replacement, so partial completion is real. Give each container its own pending record and its own pair of recorded identities under step 6, and resolve each independently under steps 9 and 10. Because a single atomic replacement either lands whole or not at all, this confines partial state to the boundary between containers, where a per-container witness can decide it, and removes it from inside any one file.

    Three rules follow. Only the verdict survives recovery, never the plan: a not-landed container discards its recorded plan and re-derives the comparison under step 8 against the file as it exists now, because the recovery window is unbounded and the developer may have edited that container since. Resolving a pending record is not executing it: if the next command is an install rather than a retried uninstall, recovery reconciles the journal for each container and stops, so a stale removal cannot fire in the middle of an install. And terminality is per container but deletion of the journal is global, so an uninstall that finished three containers and left one undecided is not complete and must exit with the status distinct from success.

    Because every effect is decided from current state under one shared removal procedure, a re-run after any interruption converges rather than compounding, which is what makes a partial uninstall safe to simply retry.

## 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. Detection of stripping is not a one-time probe: a host strips only when it next rewrites on its own schedule, so a probe read back promptly always reports that the key survived. Treat the probe as unproven until the file has demonstrably been rewritten by the host at least once, revalidate it when the host version changes, and while it is unproven prefer undecided over the verdict that authorises writing.
- 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.
- Making each container a commit unit bounds partial completion but does not make the command atomic across containers. A developer who interrupts an uninstall can observe a state in which some grants are gone and some remain, and must be told that in the report rather than left to infer it.
- 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.
- Restricting deny coverage to exact values means a pattern deny that genuinely shadows an owned value will not withdraw it. The entry stays owned and granted-in-name-only until the exact test matches, which is the safe direction but is not the same as correct.
- Ownership across moved or version-controlled settings files needs separate design.

## Evidence status
Reasoned design only, refined across five 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; the per-container commit units in step 14 inherit that uncertainty, since they multiply the same witness rather than replacing it.

## 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.
- Crash injection between containers during an uninstall that spans several, with a developer edit applied to a later container inside the window, checking that the re-derived plan removes nothing the developer authored and that the run converges on retry.
- 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.
- An interrupted uninstall followed by an install rather than a retried uninstall, checking that the journal is reconciled and no removal fires.
- 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, including a pattern deny that covers an owned value without matching it exactly.
- 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 only on its own later rewrite, 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 from first principles 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 why this change is narrow. Already covered and left unchanged: read-only commands writing nothing, which the reviewer proposed as a fix for an observation-time tombstone consuming the one re-add offered by repair; the exclusive lock and the per-invocation fallback for concurrent runs; preserving file mode and writing beside the resolved target on the same filesystem; the clean-read precondition that stops a host migration from lapsing every entry at once; and the empty-effect precondition on the landing verdict.

One reviewer claim was checked and not adopted as stated. It argued that a concurrent run makes the landing verdict unsound because the second run's witness would be read as the first run's. That does not follow, because each run records the identity its own temp file will carry and compares against that, so the first run sees its own witness absent and reaches undecided rather than a false landed. The underlying concern about concurrent writers is real and the lock already addresses it.

Three findings survived. First, the multi-container case was simply absent. One command may perform several atomic replacements, and a crash between them is a partial uninstall in which the design's single pending record and single verdict describe only one of the replacements. The load-bearing sequence is an uninstall that removes entries from the first container, is interrupted, and on the next run either replays a plan built before the developer edited the second container, deleting a developer value, or abandons the remaining containers and leaks their grants. Making each container a commit unit removes the partial state from inside a file and confines it to the boundary between files, where an independent per-container witness can decide it.

Second, detecting once whether the host strips unknown keys is unsound. The probe is written and then read back, but a host strips only when it next rewrites on its own schedule, so an early read back always reports that the key survived. A cached false durable turns the undecided branch into a not-landed branch, and not-landed is the branch that authorizes writing.

Third, the removal procedure said an owned entry a covering deny now shadows is withdrawn through the shared procedure, without saying how coverage is computed. If the deny language admits patterns, computing coverage is inference, and the design elsewhere forbids inference from reaching the removal path.

## Change and rationale

Adds the multi-container case. One command that touches more than one settings file performs more than one atomic replacement, so an interruption leaves a genuinely partial uninstall that the single pending record and single landing verdict could not describe. Each container becomes its own commit unit with its own pending record and its own witness, resolved independently, so partial state exists only across containers and never inside one. Also requires the unknown-key strip detection to be revalidated rather than cached once from a probe read back too early, and bounds how a covering deny may be computed, since pattern coverage is an inference and inference may report and block but never remove.

The previous version described a single rename and a single landing verdict, which is correct for one file but silently wrong once the managed set spans several. An interruption between two containers leaves some grants removed and some present, and the journal cannot say which without a per-container witness, so recovery either re-runs a stale plan against a file the developer has since edited or gives up and leaks the remaining grants. Two narrower corrections travel with it. Strip detection read back promptly always concludes that unknown keys survive, because stripping only manifests after the host rewrites on its own schedule, and that false conclusion converts an honest undecided into the verdict that authorizes writing. And the covering-deny test was left unspecified while the rest of the design forbids inference on the removal path.
