Design question: a command-line installer merges a fixed set of exact-match tool permission allow entries into a user-editable JSON settings file that may already contain some of the same allow entries (added by the user) plus explicit deny entries. Goal: repair should re-add only missing installer-owned entries, and uninstall should remove only entries the installer actually introduced, never pre-existing user allows, and never touch or override denies.
Known reasoning so far: ownership cannot be inferred from the settings file alone, because an entry that matches the installer's list may have been user-authored before install. A separate ownership record is needed. The hard part is the two-file non-atomic update: if the process crashes after persisting the ownership record but before the settings write lands (or vice versa), a later repair/uninstall must not misattribute ownership.
Candidate approach: a two-phase intent/commit record per entry (state: intended vs. committed, plus whether the entry pre-existed at snapshot time), settings written via temp-file + atomic rename, and a reconciliation step on the next run that resolves 'intended' entries by re-reading settings and applying the recorded pre-existence snapshot. Deny entries are treated as user-owned and as a blocker: if a deny conflicts with an installer allow, skip adding the allow rather than removing the deny.
Unknowns: best handling when the user manually deletes or re-adds an installer-owned entry between runs; whether to key on exact string or a normalized form; how to detect concurrent edits (content hash / mtime compare-and-swap) without a lock the editor honors.
Refined design (reasoning only, not yet tested).
- Uninstall is a targeted patch, not a restore. Never roll back to a pre-install backup, because that would erase unrelated edits made later. Parse the current settings, remove only exact-match owned entries from the allow list, and keep every other key, the array order, and unknown fields as they are.
- Compare before remove. The journal records each owned effect as a location plus the exact installed value, or a canonical hash of it for structured values. At uninstall, an owned entry is removed only if that exact value is still present. If it is missing or changed, the installer leaves it alone and reports a conflict saying the rule was modified or removed by the user. The edited rule now belongs to the user. Entries that already existed before install, and all deny entries, are never touched. A container the installer created is removed only if the journal says the installer created it and it is now empty.
- Write-ahead ordering for uninstall. Write a pending-uninstall journal record first. It holds the base content hash, the planned removals, the entries kept because of conflicts, and the expected result hash. Then write the settings through a temp file and an atomic rename, re-checking the base hash right before the rename. Then mark the record committed, clean up other artifacts, and delete the journal last.
- Recovery on the next invocation. Nothing runs in the background. If the current hash equals the base hash, the write never landed, so re-plan. If it equals the expected hash, the write landed, so promote the record and finish cleanup. If it matches neither, re-run compare-before-remove for entries still marked owned. This is safe because the operation is idempotent: exact matches are removed and missing ones are no-ops. Report that the settings changed during the interrupted uninstall. Accepted residual ambiguity: a user who re-adds the exact same string inside the crash window will see it removed. That is fail-safe for allow rules, since it causes a prompt rather than a grant.
- With no journal, never infer ownership from content. Leave the matching entries in place and report them.
- Repair contract: repair re-adds missing owned entries unless a deny now covers them. The deny list is the user's durable way to decline an installer rule.
Corrections to the refined design above. These came from an independent review done by reasoning; no tests were executed.
- Re-install bug. Classifying from settings content on every install turns already-owned rules into preexisting ones on a second install or upgrade, so uninstall never removes them. Fix: load and resolve the existing journal first. Owned stays owned, and only rules not in the journal get classified. A dropped pending record rolls back to the prior committed state instead of erasing it.
- Repair versus uninstall contradiction. Uninstall treats a missing or edited owned rule as released to the user, but repair re-added it. That silently re-grants a broader rule the user narrowed. Fix: in repair, report missing owned rules and re-add them only on explicit request. Repair is also a settings write, so it needs its own pending journal record.
- The crash window has no time limit. Recovery runs only at the next installer invocation, which could be weeks later. Whole-file hash comparison then misreads a later user edit. An uninstall that landed but never committed can delete a rule the user deliberately re-added. An install that landed but never committed leaves owned grants marked uncertain and kept. Fix: decide per entry from evidence. If any other planned change from the same atomic write is visible, the write landed, so a still-present planned removal is a user re-add and should be reported, not deleted. Identity of the replaced file (inode) is extra evidence. A rule absent at recovery is never owned. Use one ambiguity policy in both directions. Also, the host application may rewrite the settings file itself, so an unmatched whole-file hash is common, not rare.
- Recovery must not finish an uninstall that never landed when the current command is not uninstall. If the base hash still matches, roll back to the committed install state. Read-only commands should only report pending recovery.
- Durability. fsync the journal temp file, rename it, then fsync its directory before touching settings. Do the same for the settings temp file and its directory before committing. On macOS use the full-flush fcntl, because plain fsync does not reach stable storage.
- Serialization. Parsing and re-serializing can drop comments, reorder or collapse content, or replace a symlink with a regular file. Edit surgically, or refuse when a parse and re-write of the unmodified file does not reproduce its bytes. Preserve the file mode and write the temp file next to the symlink target.
Still sound: the external journal, never inferring ownership from content, write-ahead ordering, patching instead of restoring a backup, compare-before-remove matched by value rather than array index, never editing denies, and deleting the journal last.