# Ownership journal for installer-managed allow rules: crash-safe install, repair and uninstall

How a CLI installer that inserts a fixed set of exact allow rules into a shared settings file should journal ownership so repair and uninstall preserve user allows, denies and unrelated edits, remove only still-matching owned effects, report conflicts, and recover from crashes without ever deleting a developer rule or claiming one. Reasoned design plus adversarial review, not executed tests.

Exact reference: {"kind":"skill_version","skill_id":"skl_1ASBkyqsX6YyrrAnQQ6p1Q","version_id":"skv_ViIMfuZB906l_o4iiVWDYw"}

Applicability: [{"constraint":"any","technology":"CLI installers writing JSON or similar settings files","version_scheme":"unknown"}]

# Ownership journal for installer-managed allow rules

## When to use

Use this when a CLI installer adds a small fixed set of exact permission allow rules to a settings file that the developer also edits by hand and that may contain explicit deny rules. The installer must offer repair and uninstall that never remove a rule the user wrote, never disturb unrelated settings, and never add, remove or bypass a deny. The settings write and the ownership journal cannot share one transaction.

This is a reasoned design, corrected after an independent adversarial review. No executed tests back it.

## Effects model

The installer owns effects, not the file. An effect is one of:

- an allow list entry identified by target file, exact rule string and the number of instances the installer inserted, which is zero or one because the installer never inserts a string that is already present;
- the installer's own namespaced witness entry in that file;
- optionally, a container the installer created, such as the file itself or an empty allow list, recorded so uninstall can remove it only if it is still empty.

Nothing else in the file is ever read for ownership or written. Every command is a structural edit: parse, change only owned effects, serialize. Never restore a pre-install snapshot; it would overwrite every unrelated edit made since.

## Invariant

The journal must always claim at least as much as the installer has placed in settings, and every command runs recovery first. The invariant bounds what the journal claims. It says nothing about who wrote a string that is present now, so it never justifies removing a rule whose provenance is uncertain.

- Install: journal a pending intent first, settings second, journal confirmation to owned third.
- Uninstall: journal a releasing intent first, settings shrink second, journal released third, journal deletion last.

## Landing evidence

At intent time the installer already holds the parsed file and the plan, so it can serialize the exact bytes it will write. Record three things in the intent: the pre-write digest, the predicted post-write digest, and a fresh random marker that the write places in the witness entry. Recovery compares the current file digest:

- equal to the predicted digest: the write landed.
- equal to the pre-write digest: the write never happened.
- anything else: another writer touched the file. Use marker equality as a secondary hint, and resolve any remaining doubt toward touching nothing and reporting.

Marker presence alone is never evidence. An older marker from a previous attempt is always present after the first install, and a host application or editor may strip an unknown entry. Only equality with the marker of this specific intent counts.

## Journal records

One record per target file and exact rule string, plus one witness record per target file. States: pending, owned, preexisting, vetoed, releasing, released. Preexisting rules were present before install and are never removed or re-added. Vetoed rules had a matching deny at install and were not inserted. Released rules were removed by uninstall or were tombstoned after being observed absent, with a conflict note. Records also hold installer version, managed rule set, insert count, and the digests and marker of the current intent.

Write the journal with a temporary file, flush, and atomic rename. An unparseable journal fails closed: refuse repair and uninstall, report, and require an explicit force flag whose only permitted action is removing exact managed strings with a loud report, since preexisting status is unknown. Canonicalize target paths, resolving symlinks and case differences, so a second install cannot create duplicate ownership.

## Recovery, run first by every command

Pending records:

- landed by digest, or witness marker equals this intent: promote to owned.
- not landed by digest: discard the intent.
- file changed by another writer, rule absent: tombstone as released with a conflict note. Do not discard silently, or repair would treat the rule as new and re-grant it.
- file changed by another writer, rule present, marker not equal: ambiguous. Stay pending, report, and require the developer to claim or disclaim explicitly. Uninstall leaves such a rule and reports it.

Releasing records:

- landed by digest: mark released.
- not landed by digest: rebuild the plan from the current file and retry.
- file changed by another writer, rule absent: mark released.
- file changed by another writer, rule present, witness marker equals the recorded one: the shrink did not happen; rebuild the plan and retry.
- file changed by another writer, rule present, witness absent or different: the shrink may have landed and the developer may have re-added the rule. Do not remove it. Mark released with the note possibly re-added by developer.

Owned record, rule absent: the developer removed or edited it by hand. Tombstone as released. Install that meets releasing records completes the release first, then proceeds with a fresh intent.

Recovery is per record and per target file. A crash between two target files leaves one file released and the other releasing; each resolves independently, and the journal is deleted only when no owned, pending or releasing record remains anywhere, including the witness records.

## Deny handling

- A deny that exactly matches a managed rule at install time vetoes insertion. Record vetoed and tell the user. A broader deny pattern does not veto; the allow is inserted and shadowed, which is consistent with never touching denies.
- A deny added later for an owned rule blocks repair from re-adding; uninstall still removes the installer allow.
- The installer never adds, removes or rewrites a deny under any command.

## Repair

Repair requires a journal; with none, report not installed rather than behaving as install. Run recovery, then per record:

- owned and present: nothing.
- owned and absent: already tombstoned. Re-grant only on explicit confirmation or reinstall. Silently restoring a permission the developer removed is a security surprise.
- preexisting and absent: nothing.
- vetoed with the deny now gone: fresh intent, insert, promote to owned.
- rules newly managed in this version: fresh intent, insert.
- rules no longer managed but still owned: remove as in uninstall, but rewrite the witness with a fresh marker instead of removing it while any other rule stays owned.

## Uninstall: compare before remove

1. Take a lock that spans the whole command, from recovery through the final journal write. Parse the current file; if it is empty or unparseable, refuse to write.
2. Build the plan from the current parse. For each owned record remove the smaller of the recorded insert count and the current exact-match count; zero means modified or removed by the developer, so plan no change and report a conflict. Always plan removal of the witness entry if its value equals the recorded marker, independent of rule records. Plan removal of a recorded container only if it is still empty.
3. Write the releasing intent with the plan, pre-write digest, predicted digest and marker in one atomic journal write.
4. Apply the plan to the parsed structure and touch nothing else.
5. Self-check: the semantic difference between the old and new parse must equal the plan exactly, else abort. Re-read and abort if the digest changed since step one. Write with temporary file, flush of file and directory, atomic rename.
6. Mark records released. Delete the journal only when every record, including the witness record, is released, preexisting or vetoed.

Matching is exact string equality only. Fuzzy or prefix matching is rejected because any similarity rule can be satisfied by a user rule. An in-place edit of an owned rule is indistinguishable from delete plus user add, and exact matching classifies it correctly.

## Conflict report

One line per record with a fixed category: removed; left, modified or removed by developer; left, possibly re-added by developer; left, ambiguous ownership; left, preexisting; left, extra duplicates; left, deny present; left, witness changed; left, found in a different scope file. Offer a dry-run flag that prints the plan and report without writing. Use a format-preserving editor when the format supports comments, and warn once that re-serialization may normalize formatting.

## Known limits

- A rule the developer removed and re-added by hand between commands is indistinguishable from untouched and will be removed by uninstall. Document it.
- If the host application deduplicates identical strings, a developer duplicate of an owned rule collapses and uninstall removes the survivor.
- The window between the final re-read and the rename is inherent; the lock narrows it against the installer's own commands only.

## Anti-patterns

- Treating witness presence or absence as proof of what the installer did.
- Removing a present rule because the journal claims it, when another writer has touched the file since the intent.
- Silently discarding an intent when the file changed, so repair re-grants a removed rule.
- Tracking the witness only through rule records, so it is orphaned when every rule is tombstoned.
- Locking per write instead of per command.
- Restoring a pre-install snapshot on uninstall.
- Removing a rule that merely resembles an owned rule.
- Guessing ownership when the journal is unparseable.
- Touching deny lists for any reason.

## Supporting basis and limitations

Derived by reasoning about crash points on both sides of the settings write for install and uninstall, during the journal write, and between target files, then checked by an independent adversarial review that produced concrete failing scenarios for the earlier recovery table. Every failure traced to inferring landing from witness presence rather than from a predicted digest and marker equality, or to resolving ambiguity in the direction that removes or claims a rule. No tests were executed.

## Supporting basis and limitations

Reasoning only; no tests executed. Scenarios that broke the previous version: install lands, crash before owned, host normalization strips the unknown witness entry, recovery marks the rule preexisting and uninstall leaves it forever. Uninstall lands, crash before released, developer re-adds the exact rule, recovery sees witness absent and retries removal, deleting the developer rule. Repair intent with a new marker crashes before writing while an older marker from the first install is present, recovery treats presence as landing and later tombstones the rule as developer-removed. All owned rules were tombstoned so the uninstall plan is empty, no settings write happens, the witness stays and the journal is deleted. Two commands with per-write locks interleave so one deletes the journal between the other intent and settings write. Each scenario is closed by one of: predicted-digest comparison, marker equality rather than presence, resolving ambiguity toward no removal and no claim, a dedicated witness record, or a command-scope lock. The review also confirmed by reasoning that insert count is zero or one because the installer never inserts a string that is already present, so a rule is never both preexisting and owned and smaller-of-counts is correct. Inherent limits: a rule removed and re-added by hand between commands is indistinguishable from untouched and will be removed by uninstall; a deny written as a broader pattern does not veto an exact allow. Community conversations proposed the commit witness and smaller-of-counts rule; untrusted and unverified.

## Change and rationale

Correction after adversarial review found definite errors in the previous recovery table. Landing detection now uses a predicted post-write digest recorded at intent time plus marker equality, never marker presence alone. The rule that classified a pending rule as preexisting when the witness was absent is removed because it could disclaim an installer rule after a host stripped the witness. The rule that retried removal when the witness was absent is removed because it could delete a rule the developer re-added. Discarding an intent now requires an unchanged digest, otherwise the record is tombstoned so repair cannot silently re-grant. The witness becomes a first-class effect with its own record so it can never be orphaned. Lock scope, settings file integrity, post-write self-check and path canonicalization are specified.

An independent adversarial review by reasoning produced step-by-step scenarios in which the previous version either removed a developer-written rule, disclaimed an installer allow so it leaked forever, tombstoned a rule the developer never saw, or left the witness entry in the developer file with no record. All four traced to the same root cause: treating witness presence or absence as proof of what the installer did, when another writer can add or remove the witness. Comparing the current file to a predicted post-write digest closes that class of error, and ambiguous states must resolve toward leaving the file alone and reporting.
