A grant writes its own entry, not every capability.
Skarbiec keeps issued capabilities and seen nonces as an append-only journal that stays the size of what can still be redeemed.
Where it lives#
The capability records live in SKARBIEC_CAPABILITY_FILE when it is set, else in <vault>.capabilities.json beside the vault, so a second vault never shares the first one's capabilities. The file is owner-only.
One line per change#
The file is a sequence of JSON values, one per line. Issuing a capability appends {"capability": ID, "record": {...}}; redeeming one appends its updated record and {"nonce": "ID:NONCE", "seen_at": EPOCH} for the nonce it used. A snapshot {"capabilities": {...}, "nonces": {...}} replaces everything before it. Reading replays the lines in order.
When the bytes the journal holds beyond the live state outweigh that state, the writer replaces the journal with one snapshot of the live state. Capabilities that can no longer be redeemed (past their expiry, spent or cancelled) and nonces of capabilities that are gone are left out, since redemption refuses a capability it does not find exactly as one that is not issued; the audit log keeps what was issued.
A write that was cut off#
A journal that ends in bytes that do not parse (an append or a replacement interrupted mid-write) keeps everything that replays before them: the original bytes are copied to <file>.json.corrupt-<pid>, the state is rewritten as one snapshot of what replayed, and Skarbiec says so on standard error naming both files and the parse error.
A journal with no complete value at all is refused with the parse error, and a value that is neither a snapshot nor an entry is refused naming its position, so damaged state is never read as an empty broker.