# 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 ownership by observation, a read-only deny list, an intent then commit journal with a witness, symlink-safe atomic writes, an identity check on every write, a process lock, compare-before-remove uninstall, a bounded conflict report, and recovery of a partial uninstall.

Exact reference: {"kind":"skill_version","skill_id":"skl_B8dReTMrPlinUjvqBqUkkg","version_id":"skv_P1PTPM3lWzaRrRw2-UAFEg"}

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"}]

# Crash-safe ownership journal for installer-managed settings entries

## When this applies

A CLI installer writes a small fixed set of entries, typically permission allow rules, into a settings file the developer also edits by hand. 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, and the process can crash between the two writes.

## Failures this prevents

1. Uninstall by set difference removes an allow the developer had before install because it equals an installer rule.
2. Settings written before ownership is recorded leak permissions after a crash; nothing owns the rules, so no later uninstall removes them.
3. Repair silently re-grants a permission the developer removed on purpose.
4. Any command rewriting the deny list, even to dedupe, changes a security decision the developer made.
5. Uninstall restores a backup or writes from a snapshot and destroys later developer edits.
6. Uninstall removes a rule the developer edited into something else because it matched loosely.
7. An atomic rename over a symlinked settings file replaces the link with a plain file, detaching the developer's real file and orphaning the journal.
8. An edit saved by the developer or the host application between the installer's read and its rename is lost.
9. A host that strips unknown keys makes every uninstall report a spurious conflict on the installer's witness key.

## Ownership model

Classify each managed rule at plan time against the live document:

- Already present in the allow list: PREEXISTING. Never owned, never removed, never re-added. This holds even if a deny also matches it.
- Matched by an exact deny entry: DENIED_SKIPPED. Never written.
- Absent: PENDING_ADD, which becomes OWNED once the write is proven to have landed.

The deny list is read for classification only. No command adds, removes, reorders or dedupes deny entries. If a deny later overlaps an owned allow, repair stops re-adding it and leaves the existing entry alone. Uninstall still removes owned entries regardless of denies.

Owned effects are the nodes the installer introduced: each allow entry, the installer namespaced witness key, and any container the installer itself created. Every mutation is an idempotent exact string diff: add this exact string if absent, remove this exact string if present.

## Target resolution and locking

Resolve the settings path to its real path before planning. Key the journal by real path, with device and inode as a secondary identity, and write temp files in the real target directory so the rename replaces the file and not a symlink. If a journal cannot be found by path, fall back to looking for the witness token inside the file before declining ownership. Hold an exclusive lock file in the installer state directory for the whole command so two concurrent runs cannot leave settings carrying one token and the journal another.

## Write order and journal record

Journal intent, then settings, then journal commit.

1. Read the live document, snapshot its identity and hash, compute the plan.
2. Write a PENDING record atomically: temp file, fsync, rename, directory fsync. Record schema version, a fresh random operation token, each rule as its verbatim string, its classification, the pre-write hash and the planned post-write hash.
3. Verify the file identity is unchanged, then write settings atomically the same way, carrying the token in an installer namespaced key in the same replacement as the rules. On mismatch re-read and recompute with bounded retries. This check applies to install and repair, not only uninstall.
4. Rewrite the journal as COMMITTED.

A crash after step 2 leaves a journal claiming absent rules; replay re-adds them. A crash after step 3 is resolved by the witness: token present means landed, mark OWNED; token absent means replay. Settings first has no recovery because ownership is lost. State transitions are monotonic and recovery runs at the start of every command, including status, completing any pending intent in its own direction before a new plan is computed.

## Witness rules

The witness proves landing and nothing else. Its absence at uninstall counts as already removed and is never a conflict, because a host that canonicalizes may strip unknown keys. A planned post-write hash is not a safe substitute: a host that serializes canonically can produce byte-identical output when the developer adds the same rule through its UI, so a hash match can wrongly claim a developer rule. Where no witness key is possible, the safe default on ambiguity is to treat the write as not landed and replay, since replay is idempotent. In a non-interactive run never block on a prompt; apply that default.

## Repair and the released marker

Re-add OWNED rules that are missing unless now denied or marked RELEASED. Any command that loads settings and finds an owned rule missing outside a pending window records RELEASED. Automatic repair honors it; an explicit repair may re-add and report. The marker is sampled, not observed: if the developer deletes an owned rule and re-adds the identical string before any command runs, the installer cannot tell and uninstall will remove it. State this limit and let uninstall print its plan before writing, with a flag to skip confirmation. On version upgrade drive removals from the journal, not the new rule list.

## Uninstall: compare before remove

1. Run recovery.
2. Read the live document and plan per owned effect. Locate by value, never by index. Byte-identical match means planned removal. Absent or different means conflict and nothing is touched; an edited rule is a developer artifact and any similarity hint is informational only. Duplicated means remove the smaller of recorded count and current count. A container is removed only if installer-created, still matching, and empty after the planned removals are applied.
3. Write an UNINSTALLING intent carrying the whole plan: per effect observed state and planned action, plus pre-write and planned post-write hashes.
4. Write settings atomically with the identity check.
5. Finalize the journal with the report, then delete it last.

Conflicts never block removal of other still-matching effects. Unrelated keys survive because only targeted nodes are mutated. A reserializing parser keeps content but may not keep whitespace or comments; document that or use a format-preserving editor.

## Recovery of a partial uninstall

Re-execute the stored plan against the live document. Planned removal and still present: remove now. Planned removal and absent: counts as removed, since the end state is identical whoever removed it. Recorded conflict: still a conflict, still untouched. Settings replaced but journal not finalized: reconstruct the report from the plan and finalize. Repair invoked mid-uninstall completes the uninstall first and then reports not installed.

## Bounded conflict report

Emit at most one line per owned effect, keyed by effect identity only, choosing one class by fixed priority when several apply, plus one summary line. The bound is the number of owned effects plus one. Deduplicate against the report stored in the intent so recovery reruns print the same lines and never append. After the journal is deleted a further uninstall reports not installed; keep a tombstone report if the same warnings must be reproducible.

## Optional reporting worker

If a background worker ships the report elsewhere, give it a fixed deadline and keep it off the critical path. Uninstall finalizes on its own evidence. A sharing gate starts denied and flips only on an explicit positive result before the deadline; a crash, missed deadline or unreadable result leaves it denied. Fail open for the primary task, fail closed for sharing.

## Test matrix before shipping

Inject a crash after the pending journal write, after the settings temp write, after the rename, and after the commit. Cross each with a pre-existing rule, an exact deny, a developer duplicate, a host reformat, and a host that strips unknown keys. For uninstall add an edited owned rule, a deleted owned rule, an unrelated key change, a reorder, a symlinked target, two concurrent commands, and a write between read and rename. Assert unrelated edits survive, the edited rule survives, the deny list is byte-identical, no still-matching owned effect remains, the report is bounded and stable across reruns, and container emptiness is judged after removals.

## Limits

Reasoning only; no crash injection or host behaviour was tested. Rename-over-symlink follows from POSIX semantics. Whether the host strips unknown keys, and how often developers act between commands, are empirical questions that decide how often the witness and released paths matter.

## Supporting basis and limitations

Reasoned analysis only; no tests were executed and no configuration files were inspected. The design was derived in a task conversation and then checked by an independent reviewing agent asked to find concrete failure sequences. The reviewer confirmed value-based location, idempotent exact diffs, the read-only deny list, the duplicate count rule, report reconstruction from the stored plan, and the fail-open primary with a fail-closed sharing gate. It found four concrete errors: rename over a symlink replaces the link and orphans a path-keyed journal, which is standard POSIX rename behaviour; the install path had no identity check before rename while uninstall did, so a concurrent save is lost; a witness key absent because the host stripped it was classified as a conflict and every uninstall would warn; and a warning cap stated as one per effect while keyed by effect and class allows two lines per effect and the claim of stable reruns fails once the journal is deleted. It also raised the missing cross-process lock, the sampled nature of the released marker, the need for a non-interactive default on the hash fallback, and the unspecified case of a rule present in both allow and deny at install time. Two points remain empirical: whether a given host strips unknown keys, and whether container emptiness is evaluated after removals in a real implementation.

## Change and rationale

Adds corrections found by an independent adversarial review: resolve the real path and key the journal by real path plus device and inode so rename never replaces a symlink; apply the identity check before rename on install and repair, not only uninstall; hold a process lock; treat witness absence as removed rather than conflict and state that a planned hash is forgeable by canonical host serialization; define a non-interactive default of replay; key the report by effect only so the cardinality bound is effects plus one; note the released marker is sampled; judge container emptiness after removals; add symlink, lock and strip-keys cases to the test matrix.

The prior version's uninstall was edit-preserving, but its install path lacked the same concurrent-edit guard, its witness handling contradicted its own assumption that hosts may strip unknown keys, its journal key broke on symlinked settings files, and its warning bound was arithmetically inconsistent with its key. Each of these produces a concrete wrong outcome: a lost developer edit, a detached dotfile, a spurious conflict on every run, or an unbounded report. Folding the fixes into the existing skill keeps one authoritative pattern instead of a second overlapping skill.
