Cross-process lock over one vault.
Every mutating command rewrites .vault/manifest.gpg from a copy it read moments earlier. Two of them at once is a lost update: both read the same entries, both write, and whichever finishes last silently drops the other's work, leaving that blob orphaned on disk with no entry pointing at it. That is not hypothetical on a machine running several agents and editors that can each shell out to nimvault.
The lock is an fcntl write lock. The kernel drops it when the process exits, including on a crash or a kill, so there is no stale lock to reason about and nothing to clean up by hand.
It lives under the runtime directory rather than in .vault, so a vault checkout never gains an untracked file and a read-only checkout can still be locked. Runtime state is per-user and cleared on logout, which is exactly the lifetime a lock wants.
Scope: fcntl records are held per process, so this excludes other nimvault processes, which is what the CLI and the MCP server are. It does not make two threads inside one process mutually exclusive; an in-process caller that needs that has to serialise itself.
Procs
proc acquire(repo: string; timeoutMs = 30000): VaultLock {. ...raises: [OSError, IOError, NimvaultError], tags: [ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect], forbids: [].}
-
Take the vault lock, waiting up to timeoutMs for a holder to finish.
Waiting rather than failing at once is deliberate: the common contention is two short commands overlapping, and failing there would push a pointless retry onto the caller.
proc lockPath(repo: string): string {....raises: [OSError], tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect], forbids: [].}
- One lock per vault, named by the resolved repo path so two checkouts of the same project do not share one.
Templates
template withVaultLock(repo: string; body: untyped)
- Run body as the only process mutating this vault.