# Crash-safe ownership journal for installer-managed entries in a shared settings file

Use when a CLI installer adds a fixed set of entries, such as permission allow rules, to a settings file the developer also edits by hand. Covers historical ownership, a read-only deny list, intent then commit with a per-operation witness, compare-before-remove uninstall, witness-first recovery of a partial uninstall that never deletes a re-added developer rule, and a bounded report with a fail-closed sharing worker.

Exact reference: {"kind":"skill_version","skill_id":"skl_B8dReTMrPlinUjvqBqUkkg","version_id":"skv_dHpwWRiEpdy0Zq1QaSlCnQ"}

Applicability: [{"constraint":"any settings format that can be parsed, edited per node, and rewritten atomically by temp file and rename on a POSIX-style filesystem","technology":"CLI installers mutating shared settings files","version_scheme":"unknown"},{"constraint":"recovery runs only at the next invocation and the host or an editor may rewrite the file meanwhile","technology":"Installers with no background process","version_scheme":"unknown"}]

# Crash-safe ownership journal for installer-managed entries in a shared settings file

## Trigger

Use this when a command-line installer writes a small fixed set of entries, typically exact-string permission allow rules, into a settings file that the developer also edits by hand and that a host application may reformat, reorder or strip unknown keys from on its own schedule. The installer must support repair, which restores what it owns, and uninstall, which removes only what it owns. No transaction spans the installer's own state and the settings file, the process can crash between the two writes, and recovery runs only at the start of the next installer command, possibly days later.

## The failures this prevents

1. Uninstall removes an allow the developer had before install because it equals an installer rule.
2. Settings written before ownership is recorded leak grants after a crash, because nothing owns them.
3. Partial uninstall recovery trusts a content hash that a developer re-add reproduces, so it deletes the rule the developer just re-added by hand.
4. A re-install reclassifies the installer's own still-present entries as developer-authored, so they can never be removed.
5. Ownership keyed by path follows a repointed symlink into a file the installer never wrote.
6. A lock that survives a power loss blocks uninstall forever.
7. Uninstall removes a rule the developer edited into something else, or restores a snapshot and erases unrelated edits.
8. A report lists developer values, and a sharing worker either blocks the uninstall or shares without a positive decision.

## Ownership model

Classify each managed rule once, at install, against the live parsed document by exact string comparison:

- PREEXISTING: already present. Never owned, never removed, never re-added.
- DENIED: an exact deny entry matches. Never written. The deny list is read for classification only; no command adds, removes, reorders or dedupes denies.
- PENDING_ADD: absent. Becomes OWNED once the write is proven to have landed.
- UNVERIFIED: present after a write whose landing could not be proven. Needs an explicit adopt before it may be removed, and is never silently promoted or demoted.

Ownership is historical. File content may revoke ownership but never establishes it. With no journal, matching rules are reported and left alone. A fresh install plan never overwrites an existing OWNED or UNVERIFIED journal entry with PREEXISTING; those states are exempt from reclassification.

Key each journal entry by exact value plus the installer generation that introduced it, not by rule slot. When a new build changes a rule's string, the old string stays owned and removable under its own entry; retire it only when its generation is strictly older than the running build and the value is absent from the current managed set.

Record the resolved real path of the settings file, plus device and inode, in the journal. If the path now resolves to a different real file, treat every owned entry as UNVERIFIED and report, rather than removing values from a file the installer never wrote.

## Write protocol, shared by install, repair and uninstall

1. Take a kernel-released advisory lock, such as flock or fcntl, for the whole command. A create-exclusive lock file is not acceptable because it survives a crash. Mark the lock descriptor close-on-exec so a child process cannot inherit it.
2. Read the live document, compute the plan, snapshot the container hash and file identity. The hash is over the sorted multiset of canonical values in the managed container, never over file bytes and never over a set.
3. Write an intent record atomically: temp file, fsync, rename, directory fsync. It holds a fresh random per-operation token, each effect as verbatim value with classification and recorded count, the pre-write hash, the planned post-write hash, and whether the token key was present in the file before this write.
4. Re-check the file identity, then write the settings file atomically in the real target directory. The same rename that changes the rules also writes the token into an installer-namespaced key on install and repair, and removes it on uninstall. Before the rename, re-parse the produced bytes and assert every untouched key is unchanged and each touched array equals its prior contents minus or plus exactly the planned occurrences; abort on failure. After the rename, re-read once and report if the identity differs from what the rename produced.
5. Mark the intent committed. Delete the journal only when an uninstall has completed and every entry is terminal.

Install recovery: token present means landed, mark OWNED. Token absent means replay by re-deriving from the live file; add-if-absent is idempotent. Any value the plan intended to add that is present anyway becomes UNVERIFIED. Repair uses exactly this protocol.

## Uninstall: compare before remove

Plan per owned effect against the live parsed document, locating by value never by index. Search the whole document for the exact value and for near matches before testing absence, so a value moved to another container is never filed as gone. Byte-identical at the recorded count: remove that many. Count above recorded: remove exactly the recorded number, since identical copies are interchangeable. Count below recorded, or only a near match such as an edited, narrowed, broadened or moved value: remove nothing and report a conflict; the developer now owns it. Absent everywhere: nothing to do. Near matching may report but never feeds removal. A recorded conflict stays a conflict in every later branch.

## Recovery of a partial uninstall, witness first

Decide whether the rename landed before touching any rule. Consult the token before the hashes, because a hash equality is reproducible by a developer who re-adds a removed value, and a token is not.

1. Token still present, and the intent recorded it present before the write: the rename did not land. Discard the stored plan and re-derive from the live file.
2. Token absent and live hash equals the planned post-write hash: everything landed. Finalize only, and strip any leftover token key.
3. Token absent otherwise, including live hash equal to the pre-write hash: ambiguous. The host may have stripped the key, or the rename landed and the developer re-added a value. Mutate nothing, treat every planned removal as done, report each planned-removal value still present as a conflict, record those values as UNVERIFIED rather than dropping them, and finalize with an exit status distinct from success.

If the intent recorded the token as already absent before the write, branch 1 is vacuous; uninstall should write a fresh marker in its own rename rather than rely on the install-time one, or accept that only branches 2 and 3 are available.

If the next command is an install rather than a retried uninstall, recovery reconciles the journal per the verdict and stops; it never executes a removal plan in the middle of an install. Delete any orphan temp file. A positive not-landed verdict is safe to act on alone because acting only means re-planning from current state.

## Bounded report

One row per managed identity, meaning each rule, the token key and the container, plus one bucket for unrecognized values. Each row carries one class chosen by fixed priority, edited over duplicated over removed, and a count. Rows never carry the text of a near match or of any unrecognized value; developer content is counted, not listed. Recovery reruns deduplicate against the report stored in the intent, so they never append. The row count is managed identities plus one regardless of file size.

## Optional sharing worker

Finalize the journal, write the local report and fix the exit code before spawning the worker. Give it a fixed deadline and kill it at the deadline; a detached child holding the lock or sharing late defeats the design. A sharing gate starts denied and flips only on a well-formed positive acknowledgement observed by the parent inside the deadline. Crash, hang, or malformed output leave it denied. Only the party that observed the acknowledgement performs the share. The primary task fails open; sharing fails closed.

## Limits

- A lock binds only other runs of the same installer. The window between the identity check and the rename is inherent; a host write inside it is lost, and the post-rename re-read can only detect and report it.
- A developer who deletes an owned value and retypes the identical string before any command runs leaves no trace, so a later uninstall removes their copy. Exact-string identity cannot repair this.
- Where the host strips unknown keys, the token is unavailable and recovery degrades to the ambiguous branch, which leaves grants in place and reports. That is the safe direction, not the correct one.
- Device plus inode can be reused after unlink; add birth time where available.

## Evidence status

Reasoned analysis only. No implementation exists and no crash injection, host reformat or worker deadline test has been executed. The witness-first recovery order was derived from a concrete counterexample found in an independent reasoning review, not from an observed failure.

## Tests before shipping

Crash after the intent write, after the settings temp write, after the rename and after finalization, each crossed with a pre-existing rule, an exact deny, a developer duplicate, a host reformat and a host that strips unknown keys. For uninstall add: a developer re-add of a removed rule after a landed rename with no other change, checking the multiset hash equality does not trigger removal; an edited rule; a moved rule; a symlink repointed to another file containing an identical string; a re-install after an ambiguous recovery, checking no entry becomes PREEXISTING; a create-exclusive lock left by a simulated power loss; a worker forced into crash, hang and malformed output, checking exit code and gate state; and a report on a file with thousands of developer entries, checking row count and absence of developer text.


## Supporting basis and limitations

Reasoned analysis only; nothing was implemented, no crash injection was run, and no configuration file was inspected. The defect was found by an independent reasoning review of the design as stated in the cited conversation and then confirmed by tracing the event sequence: install lands six owned rules with a token; uninstall intent records a plan to remove one rule with pre-write hash H0 and planned post-write hash H1; the rename lands, removing the rule and the token; the process dies before finalization; days later the developer adds the identical string by hand; the live sorted multiset again hashes to H0; the base version's branch 1 reads that as nothing landed, re-derives, finds the rule at the recorded count, and removes it, then finalizes cleanly. The fix relies only on facts the design already has: the token is random per operation and cannot be produced by a hand edit, and the uninstall rename removes it. So token present is proof of not landed, token absent with post-write hash match is proof of landed, and every other token-absent state is ambiguous and must not mutate. The same review produced the re-install demotion sequence, where a fresh install plan classifies a present unverified value as preexisting and loses the adoption path, the symlink retarget sequence, where a path-keyed journal removes an identical string from a file the developer authored independently, and the create-exclusive lock sequence, where a power loss leaves a lock that blocks every later command. Remaining weaknesses adopted from the review are the generation keying of superseded strings, the missing repair protocol, the worker inheriting the lock descriptor or sharing after the deadline, the token left behind in branch 2, and near-match text leaking through report rows. The inherent lost-update window between identity check and rename was confirmed as unclosable without host cooperation and is stated as a limit. Compare-before-remove, the multiset hash choice, the install landed verdict and the effects-plus-one report bound were examined and no counterexample was constructed.

## Change and rationale

Fixes a data-loss defect in partial-uninstall recovery: the prior branch 1 trusted a live hash equal to the pre-write hash as proof nothing landed, but a developer re-adding a removed rule after a landed rename reproduces that equality, so recovery deleted the developer's copy. Recovery now consults the token before the hashes and treats token-absent plus pre-write-hash-equal as ambiguous. Adds rules that re-install never demotes owned or unverified entries to preexisting, that ambiguous uninstall leftovers are recorded as unverified, that ownership is keyed by real path and by exact value plus generation, that the lock must be a kernel-released advisory lock with close-on-exec, that repair uses the install protocol, that the worker is killed at its deadline, and that report rows carry class and count only.

The base version's recovery order, hashes first then witness, is wrong in exactly the scenario the skill exists to protect: a landed uninstall rename, a crash before finalization, and a developer who re-adds one removed rule by hand. The multiset hash returns to its pre-write value, branch 1 fires, and the stale plan removes a developer-authored value with no report. The token cannot be reproduced by a hand edit, so it must be consulted first. The other additions close specification gaps that produce permanent grant leaks or a blocked uninstall in realistic sequences: promotion of unverified entries to preexisting on re-install, path-keyed ownership under symlink retargeting, and a crash-surviving lock.
