# 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, allow-scoped compare-before-remove uninstall, positive-witness recovery of a partial uninstall, and guaranteed exits from every journal state. Reasoned analysis, not executed tests.

Exact reference: {"kind":"skill_version","skill_id":"skl_B8dReTMrPlinUjvqBqUkkg","version_id":"skv_H324Xq5vQeJl-mUHc9xvpw"}

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 fields 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 tied to a 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. Uninstall removes a deny because the developer moved the owned string into the deny list and the removal search covered the whole document.
9. An in-place editor save between the plan read and the rename keeps the inode, passes an identity-only check, and is overwritten.
10. Identity tied to device and inode across commands is invalidated by every atomic editor save.
11. Repair promotes a developer-authored value to owned when a deny is lifted, or the ambiguous recovery branch deletes the journal and orphans every installer grant.
12. A conflict record has no exit, so the journal is never deleted and every later uninstall reports a conflict that no longer exists.
13. The ambiguous recovery branch never disposes of its intent, so install, repair and uninstall all refuse forever once a host strips the marker.
14. Recovery cannot tell a crash before the rename from a landed rename whose marker the host later stripped, and the optimistic reading deletes a rule the developer re-added.
15. A newer build retires an old-generation record while its string is still in the file, orphaning the grant.

## Ownership model

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

- PREEXISTING: already present in the allow container. 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 never overwrites an existing OWNED or UNVERIFIED record with PREEXISTING.

Terminal states are RELEASED and RETIRED only. Every non-terminal state must have a documented exit, listed under Uninstall below. The journal is deleted only when every record is terminal.

Identify each record by exact value, the installer generation that introduced it, and an attempt sequence, so a reinstall after a conflict never collides with the lingering record. Uninstall removes as many exact copies of a value as it holds OWNED records for that value, capped at the number present.

RETIRED is reachable only from RELEASED or RELEASED_WITH_CONFLICT, never directly from OWNED. Every command first treats OWNED records whose generation is older than the running build and whose value is absent from the current managed set as removal candidates, exactly as uninstall does, and retires them only after that removal attempt has reached a released state.

File identity has two scopes. Across commands, record only the resolved real path; if it now resolves elsewhere, treat every owned record as UNVERIFIED and report. Within one command, additionally pin device and inode from the plan read. Never compare device and inode across commands.

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

1. Take a kernel-released advisory lock, such as flock or fcntl, on a dedicated lock file beside the journal for the whole command. Never lock the settings file itself, never use a create-exclusive lock file, and mark the descriptor close-on-exec.
2. Read the live document, compute the plan, snapshot the container hash, the byte digest and the within-command identity. The container hash is over the sorted multiset of canonical values in the allow container.
3. Ensure a marker exists. If the installer-namespaced marker field is absent from the live file, write one in a marker-only rename before recording any intent. A crash after this step leaves only a stray field that the next run overwrites. Without this step, recovery cannot distinguish a crash before the rename from a landed rename whose marker was stripped afterwards.
4. Write the intent record atomically: temp file, fsync, rename, directory fsync. It holds a fresh random per-operation witness, the marker value observed before this write, each effect as verbatim value with classification and recorded count, the pre-write hash and the planned post-write hash.
5. Re-read the settings file immediately before writing and abort if its byte digest or within-command identity differs from the plan snapshot. Write the settings file atomically in the real target directory. The same rename that changes the rules also writes this operation's witness into the marker field; uninstall writes its own fresh witness rather than merely deleting the install witness, so its landed verdict is a positive value match. Before the rename, re-parse the produced bytes and assert every untouched field is structurally unchanged and each touched array equals its prior contents plus or minus exactly the planned occurrences; abort otherwise. After the rename, re-read once and report if the identity differs.
6. Mark the intent committed. For uninstall, additionally mark records released, then strip the marker field in a second rename that touches nothing else, then delete the journal if every record is terminal.

## Recovery verdicts

Decide the verdict before touching any rule. Compare marker values, never mere presence of the field, because an older generation's witness may remain.

- Live marker equals this intent's witness: the rename landed. Finalize from the stored verdicts. Do not re-plan.
- Live marker equals the pre-write marker recorded in the intent: the rename did not land. Discard the stored plan and re-derive from the live file, since the plan may be days old and the developer may have edited since. Never replay a stored plan.
- Live marker absent or any other value: the host or the developer removed it. Fall through to the hash branch. Absence is never treated as proof that the rename did not land.

Hash branch: live hash equals the planned post-write hash means landed; finalize. Anything else, including equality with the pre-write hash, is ambiguous because a developer who re-adds every removed value reproduces that hash. In the ambiguous branch: mutate nothing; mark each planned removal that is now absent RELEASED; mark each planned removal still present UNVERIFIED; for an install intent mark each planned addition now present UNVERIFIED and each absent one discarded; commit the intent; exit with a status distinct from success. The intent is always disposed of, so later commands proceed. UNVERIFIED is non-terminal, so the journal survives and the grants remain adoptable.

If uninstall finds a pending install intent, reconcile it to a verdict first and plan removal only for OWNED records. If install finds a pending uninstall intent, reconcile per the verdict and stop; never execute a removal plan mid-install. Delete orphan temp files carrying the installer's prefix.

## Uninstall: compare before remove

Plan per OWNED effect against the live parsed document, locating by exact value never by index. Search the whole document for exact and near matches so a moved value is filed as moved rather than gone, but this search feeds the report only. Removal is scoped to the allow container. A value found in the deny container is never a removal candidate.

Within the allow container: one or more exact copies, remove exactly one per OWNED record. Zero exact copies with a near match such as an edited, narrowed, broadened or moved value: remove nothing, mark RELEASED_WITH_CONFLICT and report; the developer now owns it. Zero exact copies and no near match: mark RELEASED. Near matching never feeds removal.

Exits from non-terminal states, applied on every uninstall before planning: PREEXISTING and DENIED become RELEASED, since there is nothing to remove. RELEASED_WITH_CONFLICT with no exact copy and no near match becomes RELEASED. RELEASED_WITH_CONFLICT with the near match still present stays put and is reported. UNVERIFIED stays put until an explicit adopt, which turns it OWNED, or an explicit disown, which turns it RELEASED. Both adopt and disown are separate commands that name the value and never run implicitly.

The write is a node-level edit of the live document. Never regenerate the file from the plan and never restore a pre-install snapshot.

## Repair

Run recovery first, then the old-generation removal pass. OWNED and missing from the allow container and not denied: re-add. PREEXISTING and missing: nothing. DENIED with the deny now gone: add-if-absent; if the write lands, promote to OWNED; if the value is already present the developer put it there, so record UNVERIFIED and do not promote. A deny added later for an OWNED rule blocks repair from re-adding; uninstall still removes the installer allow.

## Bounded report

One row per managed identity, meaning each rule, the marker field and the container, plus one bucket for unrecognized values. Each row carries one class chosen by fixed priority, edited over duplicated over moved over removed over matched, and a count. Rows never carry developer text. Distinct exit status on any conflict. Recovery reruns deduplicate against the report stored in the intent.

## Limits

- A lock binds only other runs of the same installer. The window between the pre-rename re-read and the rename is inherent.
- A developer who deletes an owned value and retypes the identical string leaves no trace, so a later uninstall removes their copy.
- Where the host strips unknown fields, the marker step is defeated and recovery degrades to the ambiguous branch, which leaves grants in place, marks them UNVERIFIED and reports. That is the safe direction, not the correct one.
- Repair re-adds an owned rule the developer deleted deliberately. The only durable refusal is an explicit deny.
- Only one settings layer is managed.

## Evidence status

Reasoned analysis only. No implementation exists and no crash injection, host reformat or editor save test has been executed. Failures 12 through 15 were found by a second independent adversarial reasoning review and confirmed by tracing the event sequences against the previous version.

## Tests before shipping

Crash after the marker-only rename, after the intent write, after the settings temp write, after the rename, after journal release and after the marker strip, each crossed with a pre-existing rule, an exact deny, a developer duplicate, an edited owned rule, unrelated edits elsewhere, a host reformat and a host that strips unknown fields. Add: a conflict followed by deletion of the edited rule and a second uninstall, checking the journal reaches terminal; an ambiguous recovery followed by install, repair and uninstall, checking each proceeds; a landed rename with the marker stripped and one owned value re-added, checking nothing is removed; a newer build run as install over an old-generation owned string still present, checking it is removed before retirement; a same-generation reinstall after a conflict, checking no record collision.

## Supporting basis and limitations

Reasoned analysis only. Nothing was implemented, no crash injection or editor save test was run, and no configuration file was inspected. The design was restated in the cited conversation, then handed to one independent reasoning reviewer instructed to construct concrete failing event sequences and to withdraw any that a stated rule prevents. Each accepted finding was re-traced against the base text before inclusion. Conflict with no exit: install owns a value, the developer edits it into a near match, uninstall marks released-with-conflict and keeps the journal, the developer deletes the edited rule, and a second uninstall plans only over owned records, so the record never changes and the journal is never deleted. Ambiguous wedge: the host strips unknown fields, the uninstall rename lands, the process crashes before release, the developer adds an unrelated rule, and the next uninstall finds a hash matching neither snapshot; the base text never disposes of the intent, and both install and repair reconcile a pending uninstall intent and stop, so all three commands refuse until the developer hand-restores the exact post-write container. Window-one ambiguity: when the intent recorded no marker present before the write, a crash before the rename and a landed rename followed by a host strip are the same observation; the base text gives one branch that re-derives and one that never re-plans, and the re-derive reading removes a value the developer re-added for their own use. Retirement: the base predicate names only generation and managed-set membership, so a newer build's install can retire an owned record while its string is still in the file, and the terminal record is never planned for removal again. The reviewer also attempted and withdrew sequences targeting the marker strip against a later install witness, finalizing stored verdicts after developer edits, the read-only deny promise, and duplicate handling; each was blocked by a stated rule and no counterexample survived. One design choice was made beyond the review: a marker-only rename before the intent, so that the not-landed verdict always has positive evidence except where the host strips fields.

## Change and rationale

Closes two confirmed liveness defects and two underspecifications found by a second adversarial reasoning review. Every non-terminal journal state now has a documented exit, so a conflict record no longer pins the journal forever. The ambiguous recovery branch now disposes of its intent and marks absent removals released, so install, repair and uninstall are never wedged. Recovery requires positive marker evidence for the not-landed verdict, and a marker-only rename before the intent makes that evidence available; marker absence always routes to the conservative hash branch. Retired is reachable only from a released state, and every command runs the old-generation removal pass first. Records gain an attempt sequence to avoid collisions on reinstall after a conflict.

The previous version guaranteed that no developer rule is deleted and no deny is touched, but it could stop working entirely. A single hand-edited owned rule left a conflict record with no transition out, and a single host strip of the marker after a landed uninstall left an intent that every command reconciled and then refused. Both arise from ordinary developer editing between install and uninstall, which is the scenario the skill exists for. The window-one ambiguity was worse: the optimistic reading re-derives from the live file and deletes a rule the developer re-added, which is the exact loss the skill promises to prevent. The retirement gap orphans grants on any build upgrade.
