Vault format
A .murk file is a single JSON document. Every field is plaintext except the per-key encrypted values and one encrypted meta blob. This page walks through the structure conceptually. See SPEC.md on GitHub for the exact field-by-field schema.
The plaintext header
Section titled “The plaintext header”version: semver. murk validates the major version on load (a vault with a different major version is rejected); minor bumps are accepted transparently.created,vault_name,repo: informational metadata about the vault.recipients: the list of authorized public keys (age, SSH, or hardware-plugin), and nothing else. No names or emails live here. See Recipients & signatures for where those go and why.schema: one entry per key name, with adescriptionand optionalexampleandtags, plus optional lifecycle fields (created,updated,rotation_interval_days,expires_at,revoked_at) thatmurk doctorandmurk rotate --listuse to flag overdue or expiring credentials. This is public and readable without a key: it’s whatmurk infoandmurk lsshow. Key names must be valid shell identifiers.policy(optional): an agent allow-tag list. It lives in the plaintext header, at the same trust level as the recipient list, specifically so it’s readable without a key. See Grants.
Encrypted secret values
Section titled “Encrypted secret values”Each key in secrets carries up to three ciphertext fields, one age blob per tier:
shared: encrypted to every recipient (theeveryonegroup).scoped: a map of recipient pubkey → ciphertext, one entry per recipient with a private override (themetier; the field is still namedscopedon disk for backward compatibility with vaults written before that tier was renamed).grouped: a map of group name → ciphertext, for secrets whose base tier is a named group rather thaneveryone.
A secret’s base tier is exactly one of shared or a single grouped entry: assigning it to a named group drops any existing shared value. The private (me) override layers on top of whichever base tier is active. Because age determines readability from the ciphertext’s recipients, a non-member of a group can’t decrypt its grouped entry; there’s no separate access check.
The encrypted meta blob
Section titled “The encrypted meta blob”meta is a single age blob, encrypted to every recipient, holding everything that shouldn’t be visible in the plaintext header:
recipients: the only place public keys are mapped to display names.groups: group name → member pubkeys, kept out of the header so org structure (who’s in which group) doesn’t leak to a non-recipient.grants: the agent grant registry (ephemeral pubkey, scope, TTL, issuer), kept out of the header so an agent’s existence and scope don’t leak either. See Grants.mac/mac_key: the keyed integrity hash covering the vault’s contents, and the key used to compute it.signers/sig: the Ed25519 verifying-key registry and the current signature, which make tampering by a repo-writer attributable.
The mechanics of the MAC and signatures (what they protect against and what they don’t) are covered in Recipients & signatures.
Illustrative skeleton
Section titled “Illustrative skeleton”{ "version": "2.0", "recipients": ["age1abc...", "age1xyz..."], "schema": { "DATABASE_URL": { "description": "postgres connection string" } }, "policy": { "agent_allow_tags": ["agents"] }, "secrets": { "DATABASE_URL": { "shared": "<age ciphertext>", "scoped": { "age1xyz...": "<age ciphertext>" } } }, "meta": "<age ciphertext: recipients, groups, grants, mac, signers>"}All ciphertext is base64-encoded age output. policy, scoped, and grouped are only present when the vault actually uses them, so a vault that never touches groups, grants, or policy stays byte-identical to one written by an older murk version.