Skip to content
Get early access

Sync and conflicts

An environment’s entries are encrypted as a single blob and stored with a version number. Every write increments it.

You pull, you edit, you write back — and the write says which version you started from. If someone else wrote first, the server rejects yours instead of overwriting theirs.

At that point your client re-fetches, and merges.

The server cannot help here: it cannot read any of the three versions. So merging happens locally, comparing your base snapshot, your edits, and what is now on the server.

Most of the time this resolves with nothing to ask you:

Situation Result
You changed A, they changed B Both applied
You both changed A identically Applied once
They added C, you didn’t touch it Kept
You deleted D, they didn’t touch it Deleted
You both changed A differently You are asked

That last row is the one that matters, and the rule is absolute: when two people changed the same key to different values, Cendarum stops and asks.

Last-writer-wins would be much simpler. It is also how a staging database URL ends up in production and nobody notices until the pager goes off. A silent wrong value in a secrets manager is an outage with no audit trail pointing at the cause.

The conflict prompt shows you which key, each side’s type, and whether the notes differ. It does not show the values side by side, and does not print them to a log.

The unit of merge is the entire entry — value, type, and note together.

That prevents a specific bad outcome: someone re-types an entry from secret to env in the same change that alters its value. Merged field by field, you could end up with the new value quietly rendering unmasked. Merged as a unit, the composite always reaches a human with both sides’ type visible.

Your client remembers the highest version it has successfully authenticated for each environment, and refuses to accept anything older. A server that tried to serve you a stale payload — replaying yesterday’s secrets after you rotated a compromised key — gets refused rather than believed.

That marker is stored durably, so it survives a restart. If the file is corrupt, the client fails closed instead of resetting the marker, because a silent reset is exactly what would reopen the replay window.

One limit worth naming: two honest writes can legitimately exist at the same version — the loser of a rejected write and the winner’s. A malicious server could use that to show different members different histories, as long as version numbers only ever grow. Detecting it needs each payload to be chained to its predecessor, which is a change to the encryption format rather than something we can patch around. It is listed in the threat model.

cendarum import participates in the same loop, and on conflict it stops:

cendarum: error: import conflicts with concurrent changes — resolve in the desktop app:
cendarum: conflicting key: API_URL

Key names only. The CLI ships no resolution interface on purpose — choosing between two versions of a secret needs both values visible and a person looking at them, and a terminal prompt is a poor place to make that call.

Each environment keeps its payload history, and you can fetch an earlier version to see what a value used to be.

With one caveat: history from before a key rotation is unreadable, because the key that decrypted it was replaced. That is the same property that makes revocation real — the old key is genuinely gone — and it is the accepted cost of it.