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 user also edits by hand. Covers classifying pre-existing entries as not owned, never touching deny lists, an intent then commit journal with a commit witness so a crash between the two writes is recoverable, and idempotent repair and uninstall.
Crash-safe ownership journal for installer-managed settings entries
When this applies
A CLI installer writes a small fixed set of entries into a settings file that the developer also edits by hand. The common case is a permissions section with an allow list and a deny list. The installer must later support repair, which restores what it owns, and uninstall, which removes only what it owns. There is no transaction spanning the installer state and the settings file, and the process can crash between the two writes.
Failures this prevents
- Uninstall by set difference deletes an allow the developer had before install because it happens to equal an installer rule.
- Writing settings before recording ownership leaks permissions after a crash. The rules exist, nothing owns them, every later uninstall leaves them in place.
- Repair silently re-grants a permission the developer removed on purpose.
- Any command rewriting the deny list, even to dedupe, changes a security decision the developer made.
Ownership model
Classify every managed rule at plan time against the current settings:
- Present in the allow list already: PREEXISTING. Never owned, never removed.
- Matched by an explicit deny, exact match at minimum: DENIED_SKIPPED. Never written.
- Absent: PENDING_ADD, becomes OWNED once the write is proven to have landed.
The deny list is read for classification only. No command adds, removes or reorders deny entries. If a deny later overlaps an owned allow, repair stops re-adding the allow and leaves the existing entry in place; deny wins at evaluation time and removing the allow is an unrequested mutation. Uninstall still removes owned entries regardless of denies.
Write order
Journal first, then settings, then journal again.
- Read settings and compute the plan.
- Write a PENDING journal record: temp file, fsync, rename, directory fsync.
- Write settings atomically the same way, carrying a commit witness.
- Rewrite the journal as COMMITTED with the final state of each rule.
A crash after step 2 leaves a journal claiming rules that are not present. That is harmless: repair re-applies, uninstall finds nothing to remove. A crash after step 3 is resolved by the witness on the next run. The reverse order, settings first, has no safe recovery because ownership is lost.
Journal record
Key the journal by the target settings file so user scope and project scope do not collide. Record:
- schema version
- a random install token generated per operation
- each managed rule as its verbatim string, never an index into the current version rule list
- the pre-write classification of each rule
- pre-write file identity or content hash
- the planned post-write content hash, computable because the exact bytes are known before writing
- a state per rule: PENDINGADD, OWNED, PREEXISTING, DENIEDSKIPPED, RELEASED, UNINSTALLING
State transitions are monotonic. A pending state never overwrites a committed one on recovery.
Commit witness
Preferred: write the install token into an installer namespaced key inside the settings file in the same atomic replacement as the rules. Token present means the write landed; token absent means it did not. No developer edit can forge either verdict.
Fallback when extra keys are not allowed: compare the current settings hash to the planned and pre-write hashes in the pending record. Planned match means landed. Pre-write match means not landed. Neither means a third party wrote in between, often the host application reformatting the file. Then infer per rule: recorded absent before and present now becomes OWNED; absent now is re-applied by install or repair and dropped by uninstall.
Recovery
Run recovery at the start of every command, including status. Resolve every pending record before computing a new plan so all commands see one consistent state.
Uninstall
- Write an UNINSTALLING intent so a crash mid-way cannot let a later repair resurrect the rules.
- Remove exactly one occurrence of each OWNED rule if present. If the developer duplicated a managed rule, remove the smaller of the recorded count and the current count so uninstall terminates instead of becoming permanently ambiguous.
- Leave PREEXISTING rules, every deny, every other key and the file itself in place.
- Finalize or delete the journal only after the settings write is durable.
Repair
Re-add OWNED rules that are missing unless the rule is now denied or marked RELEASED. The installer cannot watch a deliberate deletion happen, so any command that loads settings and finds an owned rule missing outside a pending window records a RELEASED marker. An explicit user-invoked repair may still re-add and report. Silent or automatic repair must respect the marker.
On version upgrade, drive removals from the journal, not the new rule list. Owned rules that dropped out of the set are removed; new rules are classified fresh.
Test matrix to run before shipping
Inject a crash at each of four points: after the pending journal write, after the settings temp write but before rename, after the settings rename, and after the committed journal write. Cross each point with a pre-existing rule, an exact deny, a developer duplicate of a managed rule, and a host reformat of the settings file between crash and recovery. Assert after recovery plus uninstall that the allow list equals the pre-install allow list and the deny list is byte-identical.