# 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 a partial uninstall stays recoverable.

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

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, file identity readable before and after the rename, and awareness that device plus inode may be reused after unlink","technology":"POSIX filesystems","version_scheme":"unknown"},{"constraint":"Recovery runs only at the next invocation, which may be much later, and the host or an editor 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.
- Uninstall deletes or reverts an owned entry the developer hand-edited after install, instead of treating the edit as a transfer of ownership.
- 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 reused inode after an interrupted uninstall makes a host rewrite carry the installer's own post-rename identity, so recovery declares landed, deletes the journal and leaks every grant.
- 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.
- One hand-edited rule breaks unanimous corroboration for the others, so every clearly landed addition is stranded as unverified.
- A value absent from its recorded container but present elsewhere is treated as absent, goes terminal, and becomes a permanent developer entry.
- 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.
- Repair treats a changed file identity as proof the host rewrote the file, but an editor that saves by temp-and-rename produces the same signal, so a grant the developer deliberately deleted is silently restored.
- 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 that were lost by accident.
- 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, and for the verdict that authorizes deleting the journal it may not rest on identity alone either.

3. **Record per effect** the container key path addressed by key rather than array position, the exact value and a canonical form, 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. This is what preserves unrelated developer edits: nothing ever writes from a snapshot or from a stale plan.

8. **One removal procedure, shared by every command including recovery.** Compare on parsed values, never on bytes. First search the whole document for the recorded value and for near matches of it, then decide. Present in its recorded container at its recorded count means remove exactly that many. 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, present only as a near match such as an altered pattern, a broadened or narrowed form, or the same value in a different container, means remove nothing and record a conflict; the developer's edit has transferred ownership to them. Only when neither the exact value nor a near match exists anywhere is the entry absent, which means do nothing, and it may be recorded as lapsed only under the clean-read precondition in step 11. Near matching runs before the absent test precisely so a moved or edited value cannot go terminal as absent. 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.

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 base identity means not landed, and that verdict is safe to act on alone, because a false not-landed only re-derives a plan from current state. The post-rename identity means landed only if identity carries a component that is not reused after unlink, such as file birth time together with device and inode. Where identity is device and inode alone, a matching post-rename identity is necessary but not sufficient for landed, because the orphan temp file of an interrupted uninstall can be removed and its inode reused by a later host rewrite; in that case content must also corroborate. Content corroboration requires at least two effects that are not no-ops. For removals it requires all of them absent at their recorded location. For additions it requires none of them exactly absent, where a near match counts as present, because the base count was zero and an edited value is evidence the addition arrived before it was edited. 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; 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. Then re-derive the comparison against the file as it exists now under step 8; never replay the stale plan. 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. Only an explicit per-entry override naming the exact value may remove a conflicted variant; a blanket force flag is not acceptable, since one acknowledgement over a batch removes the edit the developer meant to keep alongside the ones they did not.

    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, and only two causes are knowable: removed by this installer's own uninstall, or unexplained. A changed file identity is not evidence that the host rewrote the file, because an editor that saves by temp-and-rename changes identity in exactly the same way, so a lapse first seen across an identity change is still unexplained. Install and repair both re-add a lapse of the first kind 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 that were lost by accident.

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 built from device and inode alone is reused after unlink on common filesystems. Adding birth time makes reuse improbable but not impossible, which is why step 9 requires content corroboration for the landed verdict wherever identity may be reused. 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.
- Counting a near match as present for addition corroboration admits the case where the developer typed a similar value inside the crash window. The consequence is bounded: the entry is recorded as owned, and a later uninstall finds only a near match and keeps it as a conflict, so nothing of the developer's is removed.
- Treating every non-self lapse as unexplained means an accidental loss caused by the host requires the same per-entry request as a deliberate deletion. That is the safe direction: the request names the value and costs one confirmation, whereas the alternative silently restores revoked grants.
- 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 six 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.
- A developer hand-edits one owned rule and an unrelated key after install, then uninstalls, checking that the edited rule is kept and reported, the unrelated key is untouched, and the remaining owned rules are removed.
- A developer deletes one owned rule with an editor that saves by temp-and-rename, saves an unrelated edit later, then runs repair, checking that nothing is re-added without an explicit per-entry request.
- An uninstall interrupted between the pending record and the rename, followed by removal of the orphan temp file and a host rewrite that reuses its inode, checking that recovery reaches undecided rather than landed and does not delete the journal.
- An install whose rename landed but was never committed, followed by a hand edit to one owned rule and an uninstall, checking that the unedited rules are corroborated as landed and removed rather than stranded as unverified.
- An owned value moved by hand to a different container, checking that it is conflicted rather than lapsed.
- 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 each entry is named and requested separately.
- 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 restated in the cited conversation, refined for the case where a developer hand-edits one owned rule and unrelated settings before uninstall, and then handed to an independent reviewer that reasoned only, read no files and ran no commands. The reviewer traced that scenario and found no failure: five matching rules are removed, the edited one is kept and reported, and unrelated keys survive the surgical patch. It found one confirmed error and three gaps, all adopted here. The confirmed error is in repair's lapse attribution. The only evidence the design offered for a host rewrite was a changed file identity, and an editor that saves by temp-and-rename produces exactly that, so a developer who deletes an owned rule and later saves any unrelated edit satisfies two clean reads at different identities and repair re-adds the revoked grant with no request. The first gap is identity reuse: after an uninstall crashes between the durable pending record and the rename, the orphan temp file can be cleaned and a later host rewrite can reuse its inode, so recovery sees the post-rename identity, declares landed, marks removals terminal and deletes the journal with all grants still present. The design already listed reuse as a limit but did not connect it to the worst outcome. The second gap is that unanimous corroboration strands landed additions when one was later edited, forcing all of them to unverified. The third gap is the unstated precedence between absent and moved. One reviewer observation was noted but not adopted as a change: read-only commands cannot resolve a pending record, which only nominally contradicts the rule that every command resolves it, since the existing text already says read-only commands only report. Acknowledged limits the reviewer did not re-count are the identical-retype case, hosts that strip unknown keys, and the residual rename race with an unlocked editor.

## Change and rationale

Four corrections from an independent reasoning review of the compare-before-remove and partial-uninstall paths. Repair no longer treats a changed file identity as proof the host rewrote the file, because an editor saving by temp-and-rename produces the same signal; every lapse not caused by the installer's own uninstall is now unexplained and needs a per-entry request. A landed verdict by identity alone is no longer trusted for removals where identity is only device and inode, since inode reuse after an interrupted uninstall can forge it; content must corroborate. Corroboration for additions counts a near match as present instead of demanding unanimity, so one hand-edited rule cannot strand the others. Whole-document near matching now runs before the absent test so a moved value is conflicted, not lapsed.

The previous version reasoned correctly about the hand-edit scenario but left one confirmed error and three gaps. The lapse-cause rule offered no way to demonstrate a host rewrite except a changed identity, and that signal is produced by ordinary editors, so a deliberately revoked grant would be silently restored on the second observation. The landed verdict was declared to hold regardless of content, yet a reused inode after an interrupted uninstall lets a host rewrite present the recorded post-rename identity, and that false landed verdict deletes the journal and leaks every grant. Unanimous corroboration treated a near match on an addition as disagreement, although a near match at a base count of zero is evidence the add arrived. And the removal procedure never said whether absent or moved wins, so a moved grant could go terminal as a developer entry.
