██████╗ ██████╗ ████████╗ ██╗ ██████╗ ██████╗ ██╗ ██╗
██╔══██╗ ██╔═══██╗ ╚══██╔══╝ ██║ ██╔═══██╗ ██╔════╝ ██║ ██╔╝
██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ █████╔╝
██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██║ ██╔═██╗
██████╔╝ ╚██████╔╝ ██║ ███████╗ ╚██████╔╝ ╚██████╗ ██║ ██╗
╚═════╝ ╚═════╝ ╚═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
Encrypt your .env. Share by key, not by password.
What is DotLock?
DotLock is a local, file-based secrets manager for project environment variables. It stores secrets encrypted in a .lock/ vault next to your code — safe to commit to git — and injects decrypted values only into child processes launched with dl run or dl exec.
Use DotLock when you want:
- to keep
.envvalues encrypted inside the repository, instead of passing plaintext files around; - to run local commands with decrypted variables without ever writing plaintext back to disk;
- separate, cryptographically isolated vaults per environment (
dev/staging/prod) in the same checkout; - to share project secrets with teammates through public keys instead of a shared password;
- a lightweight, tamper-evident audit log of unlocks, runs, key rotations and provider resolutions.
DotLock is not a hosted production secret manager. For large production systems, prefer a dedicated service such as AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault or your platform's native secret store.
Contents: Install · Quick start · Core workflow · Environments · CI usage · Rotation · Recovery · Sharing · Providers · Git · Audit · Security model · Configuration · Command reference
Status
DotLock is at 1.0.0: the on-disk vault format, the CLI surface and the security model are stable, and vaults created with recent 0.1.x releases keep working unchanged. It remains a local, file-based tool — see the security model for what it does and does not protect against, and keep backups of .lock/ as ordinary operational hygiene. Release history: CHANGELOG.md.
Installation
From crates.io
The crate is named dotlock-bin; the installed binary is named dl.
From source
DotLock uses Rust edition 2024.
Quick Start
What happens:
dl initcreates.lock/vault.tomland.lock/secrets.lock(both safe to commit).dl migrate .envimports variables from an existing.envfile into the encrypted vault.dl run npm startstartsnpm startwith decrypted secrets in the child process environment — nothing plaintext touches disk.
After migration, remove the plaintext .env only when you have verified the imported values:
Core Workflow
Initialize a project — dl init
Creates the project vault in the current directory:
You choose whether DotLock generates a strong master password or lets you type one (typed passwords must pass a strength check). Vault metadata goes into .lock/vault.toml; encrypted secret records go into .lock/secrets.lock. Run once per project.
Add or update a secret — dl set
dl set NAME [VALUE] stores a secret under a normalized environment-variable name:
|
Prefer omitting the value. With no VALUE, dl set reads the secret from a hidden prompt; --stdin reads it from standard input instead. Both keep the secret out of ps, /proc/<pid>/cmdline and shell history, where an argv value is visible to other processes.
Names must use letters, digits and underscores, cannot start with a digit, and are normalized to uppercase. --alg/-a selects the encryption algorithm; the current supported value is xcha (XChaCha20-Poly1305). Aliases: dl s, dl add.
Read a secret — dl get
|
When stdout is a terminal, the value is masked by default to keep it out of the scrollback — pass --reveal to show it. When stdout is piped, dl get always prints the raw value (no flag needed). Use it sparingly for copying or debugging; prefer dl run for normal execution.
List secrets — dl list
Remove a secret — dl unset
Aliases: rm, remove, del, delete, u, d.
Run a command with secrets — dl run
dl run [--env-file FILE] -- CMD [args...] decrypts accessible secrets and starts a child process with those values in its environment. The command runs directly (argv form, no shell); secrets are injected as environment variables only and are never interpolated into the command string.
This is the normal way to consume secrets locally or in scripts.
Run a shell command line — dl exec
dl exec [--env-file FILE] COMMAND... is the shell-form sibling of dl run: the command line runs via sh -c, so pipes, && and other shell syntax work. Secrets are still injected as environment variables only — never spliced into the string.
Multiple words are joined with spaces, so dl exec npm start also works. Prefer dl run -- cmd args (no shell) when you don't need shell syntax.
Merge extra variables from a plain .env file — --env-file
Both dl run and dl exec accept --env-file FILE to load additional plaintext variables alongside the vault. It is a migration aid: vault secrets always win on a name collision, and env-file values are neither encrypted nor covered by the vault's integrity checks.
Lock the session — dl lock
Use after a sensitive session, before handing off a machine, or at the end of CI/scripted workflows.
Safety defaults
Every destructive command (dl unset, dl rotate *, dl share revoke, dl env remove, dl repair) asks for interactive confirmation and takes -y/--yes for scripts. Without a TTY and without --yes, they fail fast instead of hanging. After a git merge that touched .lock/ files, run dl reconcile.
Multiple Environments
A project can hold several environments — dev, staging, prod — each with its own independent vault pair: fresh salt, key-encryption key, project key and its own master password. Environments are cryptographically isolated: knowing the dev password gives no access to prod, and rotating or corrupting one environment never touches another.
Backward compatibility: the default environment is the vault DotLock has always used (.lock/vault.toml + .lock/secrets.lock), so existing projects keep working unchanged. Named environments live under .lock/envs/<NAME>/ with the same pair layout.
Manage environments
dl env remove destroys every secret in that environment and cannot remove the default environment.
Select the environment per command
Every command accepts the global --env NAME flag. Selection precedence:
--env NAMEon the command line;- the
DOTLOCK_ENVenvironment variable; - the selection persisted by
dl env use(in.lock/env); - the default environment.
--env default (or DOTLOCK_ENV=default) always forces the default environment.
DOTLOCK_ENV=staging
The Git merge driver is environment-aware: merges of .lock/envs/<NAME>/ files are routed to the right vault pair automatically.
Non-Interactive / CI Usage
Supply the master password without a prompt
In headless environments (CI, scripts) the interactive password prompt cannot run. DotLock accepts the master password from three non-interactive sources, in this precedence order:
--password-stdin— reads the password from the first line of stdin (sodl set NAME --stdin --password-stdincan still read the secret value from the rest of the pipe);--password-file FILE— reads the password from the first line ofFILE, opened with the same symlink-safe reader used for vault files;DOTLOCK_MASTER_PASSWORD— environment variable, used when neither flag is passed.
When a TTY is available and no source is set, the interactive prompt runs as usual. When there is no TTY and no source, the command fails with a clear error. Both flags are global and work with every command, including dl init (where the password must still pass the strength check).
# CI unlock (preferred): the password never enters the process environment.
|
# Env var (simplest, but weaker: environment variables are inherited by child
# processes, readable in /proc/<pid>/environ, and commonly echoed by CI debug logs).
DOTLOCK_MASTER_PASSWORD=""
# Non-interactive init for ephemeral test environments.
|
A minimal CI job (GitHub Actions):
- name: Run tests with vault secrets
run: printf '%s\n' "$DL_PASSWORD" | dl run --password-stdin -- npm test
env:
DL_PASSWORD: ${{ secrets.DOTLOCK_MASTER_PASSWORD }}
Security tradeoff: every non-interactive source feeds the exact same unlock path as the prompt (Argon2id key derivation, project-key unwrap, metadata MAC and rollback-epoch verification) — there is no weaker CI unlock. Prefer --password-stdin or --password-file over the env var. The password buffer is zeroized in memory in all cases.
Machine-readable output — --json
The global --json flag switches read commands to structured JSON on stdout (human output stays the default). Exit codes are unchanged.
| Command | JSON shape |
|---|---|
dl list --json |
[{"id": "<uuid>", "name": "<NAME>"}] (never values) |
dl get NAME --json |
{"name": "<NAME>", "id": "<uuid>", "value": "<value>"} |
dl share list --json |
[{"label", "fingerprint", "full_access", "allowed_secret_count"}] |
dl audit show --json |
array of full audit entries (same shape as the on-disk JSONL lines) |
dl provider list --json |
["<name>", ...] |
|
|
|
dl get --json emits the secret value only because the user explicitly asked for that exact secret — the same exposure as the default piped output.
Rotation and Ratcheting
Rotate the master password — dl rotate master-password
Changes the password used to unlock the project key. Fast, because only the wrapping changes — see the key hierarchy for why.
Use when a password may have been exposed but the vault itself does not need a full project-key rotation.
Rotate the project key — dl rotate project-key / dl rotate kek
Generates a new project key (DEK) and rewraps every secret data key under it; secret ciphertexts stay unchanged, only their wrappings move.
(kek is the historical command name — the KEK itself is always re-derived from the master password, so what actually rotates is the project key and the SDK wrappings.) Use after a suspected project-key exposure, after major access changes, or as periodic hygiene.
Policy-driven rotation — dl rotate --if-due
Rotates the project key only when a rotation is due per the configured policy, and exits 0 without rotating (and without prompting for a password) when nothing is due — cron/CI friendly. Two policies can make a rotation due:
rotate_max_age_days— age since the last rotation;auto_ratchet_after_writes— number of vault writes since the last rotation.
|
--if-due conflicts with an explicit rotation subcommand — it decides whether to rotate, not what. Schedule it so key rotation happens on a policy instead of on memory.
Automatic write-count ratchet
Independently of --if-due, setting auto_ratchet_after_writes makes DotLock rotate key wrapping automatically after that many vault writes, inline as part of a normal write command; 0 disables it. Use --if-due for a schedule you control; use the auto-ratchet to rotate without any scheduled job.
Vault Recovery (dl repair)
dl repair diagnoses and recovers a vault whose integrity hash is out of sync with secrets.lock — the state DotLock otherwise reports as tampering. Legitimate causes include lost transaction journals, partial backup restores and historical merge bugs. Repair can:
- finish or roll back an interrupted vault transaction;
- recompute and reseal a stale or undecryptable integrity hash — after verifying that every record still decrypts;
- with
--prune, remove records that are genuinely irrecoverable (missing key wrapping or failed decryption) and reseal the rest. The data loss is explicit and enumerated; without--prune, repair only reports those records and exits non-zero.
Repair requires a valid full-access unlock — it is a recovery path for people who hold the master password, never a tamper bypass. Without the correct password there is no project key and no repair. Every executed repair is recorded in the audit log.
Use it when a command aborts with a vault-integrity error and you know the cause is operational (interrupted write, restored backup) rather than an attack.
Import and Export .env Files
Import — dl migrate [PATH]
The parser accepts KEY=VALUE, export KEY=VALUE, single-quoted values, double-quoted values with escapes (\n, \r, \t, \\, \"), comments and blank lines.
Export — dl export [PATH]
Writes missing static secrets into a .env file:
Existing keys are never overwritten. Dynamic secrets are skipped because their values are resolved at runtime. dl export warns when the exported file is not covered by .gitignore inside a git repository and offers to add it.
Team Sharing
DotLock supports shared access through a local key-pair identity: the project owner keeps the master password, and recipients unlock with their own private key. New identities use Ed25519 for signing and X25519 sealed boxes for key wrapping; legacy RSA identities are still read for backward compatibility (see dl cert migrate and ADR 0001). Grants are cryptographically signed by an authorized signer's identity, so a recipient list cannot be silently extended by editing the vault file.
Create a local identity — dl cert init
Each developer or CI environment does this once before requesting shared access.
Show or export the public key — dl cert show / dl cert export-pub
Use export-pub when sending your public key to a vault owner.
Migrate a legacy RSA identity — dl cert migrate
Moves an existing RSA identity to the modern Ed25519/X25519 scheme and rekeys the current project's recipient entry so unlocking it never touches RSA again:
The old RSA key is archived next to the new identity — it is still needed to rekey other shared projects you belong to; run dl cert migrate once inside each of them, then the archived key can be retired. If the identity is already Ed25519 but the project still holds an RSA wrapping, the command performs one final RSA unwrap and rewraps the entry as an X25519 sealed box. Run once per machine if you created your identity before the Ed25519 migration, then once per shared project. Rationale and details: ADR 0001.
Enable shared mode — dl share enable
Run before granting access to teammates or CI.
Grant access — dl share grant
Without --allow, the recipient gets full access. With --allow, the recipient can decrypt only the listed secrets — use it for least-privilege CI identities.
Manage a recipient's ACL — dl share allow
The recipient query can be the recipient id, label or public key fingerprint; --add/--remove take comma-separated lists. Removing a secret from a recipient rotates that secret's data key, so the old recipient wrapping cannot decrypt future values.
List or revoke recipients — dl share list / dl share revoke
Revocation rotates project access material so the revoked identity cannot unlock future vault states. It cannot un-read the past: values the recipient could already decrypt from old git history should be rotated at the source (new API keys, new passwords) — see what DotLock does not protect against.
Dynamic Secret Providers
Dynamic secrets are resolved by external executables instead of storing a fixed value. Provider binaries must be named dotlock-provider-NAME and be available on PATH.
Discover providers
Store a dynamic secret
--config must be valid JSON. --bootstrap is a comma-separated list of existing static secrets that DotLock decrypts and passes to the provider at resolve time.
Provider protocol
Providers run as child processes:
dotlock-provider-NAME --describeprints human-readable provider information;dotlock-provider-NAME --resolvereads JSON on stdin and prints the resolved secret value on stdout.
The resolve input has this shape:
DotLock records the provider path and SHA-256 digest when the dynamic secret is created, and verifies the digest before running the provider on later resolutions. Provider stdout is limited to 64 KiB and stderr to 16 KiB. Providers are executable code — install them only from trusted sources. DotLock refuses provider directories/binaries that are group- or world-writable or owned by another non-root user, and warns loudly when a dynamic secret's provider is not sha256-pinned (re-run dl set <NAME> --provider <name> to pin it).
Git Integration
Install the merge driver — dl git install-merge-driver
Configures Git to use DotLock's merge driver for .lock/secrets.lock and .lock/vault.toml:
dl init also attempts to install it when the project is inside a Git work tree. The driver is environment-aware: merges of .lock/envs/<NAME>/ files are routed to the right vault pair.
Reconcile after a merge — dl reconcile
When the merge driver combines two vault histories, the result is intentionally left pending: DotLock records a marker and refuses normal operations until a human reviews it. dl reconcile shows what the merge changed, then re-signs and accepts the merged vault after your confirmation:
Reconcile requires a full-access unlock and refuses to proceed if the merged files were edited after the merge driver produced them. Run once after any Git merge that touched .lock/ files.
Sync from remote — dl sync
Fetches the configured remote and updates the current branch only when Git can fast-forward safely:
It uses auto_fetch_remote from project config (default origin), aborts when .lock/vault.toml or .lock/secrets.lock has local staged, unstaged or untracked changes, or when the branches diverged. It never runs a hard reset and never discards local vault data.
Auto-fetch before run
When enabled, dl run tries a short git pull --ff-only --no-rebase REMOTE BRANCH before unlocking:
DOTLOCK_AUTO_FETCH=0
If pull fails, DotLock tries git fetch and then continues with the local vault.
Audit Log
DotLock keeps a local audit log outside the project by default. Entries are hash-chained; if a local plain identity is available they are signed, otherwise they are anonymous.
Show entries — dl audit show
Verify, rotate, locate — dl audit verify / rotate / path
Verification is strict by default: anonymous (unsigned) entries and an unsigned high-water mark fail with a non-zero exit code; --lax downgrades those to warnings. Each audit write also records a signed high-water mark (entry count plus head hash) next to the log, so deleting the last N entries is detected even though each remaining line still chains correctly. Logs rotate automatically when they reach 10 MiB or are older than 90 days.
Audit logs are local by default — useful for local accountability and incident review, not a centralized compliance system.
Security Model
Threat model in one line: the primary adversary is someone with write access to the repository (a teammate, CI, or anyone who obtains .lock/), or a local process running as your user — not a remote network attacker, because DotLock is not a network service.
This section has two layers: first the design and why it is shaped this way, then the exact cryptography for readers who want to check the details.
Layer 1 — Design: why the keys are layered
DotLock uses an envelope (key-wrapping) hierarchy. No key in the chain ever touches disk unencrypted:
master password
└─ Argon2id ──► master key (in memory only, zeroized after use)
└─ HKDF ──► KEK (key-encryption key; re-derived, never stored)
└─ wraps ──► project key (DEK; random, stored only in wrapped form)
└─ wraps ──► per-secret keys (SDKs; one random key per secret)
└─ encrypts ──► each secret value / provider metadata
Each layer exists for a reason:
- Cheap password changes.
dl rotate master-passwordonly re-wraps the project key — one small blob — instead of re-encrypting every secret. - Blast-radius limiting. Rotating the project key (
dl rotate project-key) rewraps the per-secret keys but leaves secret ciphertexts untouched; compromising one secret's key (SDK) exposes one secret, not the vault. - Per-secret sharing. Because every secret has its own key, a recipient can be granted exactly the secrets they need (
dl share grant --allow): they receive wrapped SDKs for those secrets and nothing else.
What lives where (committed vs. local)
| File | Committed to git? | Contents |
|---|---|---|
.lock/vault.toml |
yes | vault metadata: salt, wrapped project key, recipients and signed grants, encrypted integrity hash, metadata MAC, epoch counter, project config |
.lock/secrets.lock |
yes | encrypted secret records: name, id, ciphertext, wrapped per-secret keys |
.lock/env |
yes (optional) | plain, non-secret persisted environment selection from dl env use |
.lock/envs/<NAME>/ |
yes | one vault.toml + secrets.lock pair per named environment |
~/.lock/identity/ |
never | your private identity key (identity.pem), public key, metadata |
~/.lock/run/ |
never | wrapped session cache + its wrapping key |
~/.lock/audit/<project>/ |
never | hash-chained audit log + signed high-water mark |
~/.lock/epoch/ |
never | per-machine rollback anchor (newest vault epoch seen) |
Everything committed is either public metadata or ciphertext. An attacker who obtains the repository sees variable names and ciphertexts — nothing decryptable without the master password or a granted private key. Secret records deliberately do not carry a per-secret algorithm tag next to the name (the cipher is a storage-version policy), so the vault leaks no name-to-algorithm pairing an attacker could use to prioritize offline guessing.
DotLock refuses to fall back to the current directory for local state: when neither HOME (or %LOCALAPPDATA% on Windows) nor DOTLOCK_HOME resolves — cron jobs, containers, systemd units without a login environment — commands that would write identities, cached keys or audit logs fail with a clear error instead of dropping key material into a committable ./.lock.
The protections and why each exists
- Confidentiality. Every secret is encrypted with an authenticated cipher under its own key; the password is stretched with a memory-hard KDF so offline brute-force of a stolen
.lock/is expensive. - Tamper detection. Vault metadata carries a MAC and
secrets.lockis covered by an encrypted hash, both keyed from material only a legitimate unlock produces. Editing either file outside DotLock aborts commands with a tamper error — an attacker with repo write access cannot silently swap ciphertexts, resurrect deleted secrets, or add themselves as a recipient. - Rollback protection. The vault carries a monotonic epoch, and each machine remembers the newest epoch it has seen. Force-pushing an older-but-self-consistent vault (e.g. one from before a revocation) is detected on any machine that already saw the newer state.
- Signed sharing. Recipient grants are signatures, not just list entries — the recipient list is not editable by anyone who can write the file.
- Audit trail. Unlocks, runs, rotations, repairs and provider resolutions are appended to a hash-chained, signed-when-possible local log with a signed high-water mark, so both middle-of-log edits and tail truncation are detectable.
What DotLock does NOT protect against
Being honest about limits matters more in a security tool than anywhere else:
- An attacker who owns your machine while a vault is unlocked. Code running as your user (or root) during the session-cache TTL can read the cache and its wrapping key, or simply read your process memory. No local tool survives that;
dl lockandDOTLOCK_CACHE_TTL=0narrow the window, they do not close it. - Revoked users reading the past.
dl share revokerotates keys so the recipient cannot unlock future vault states — but every value they could decrypt before revocation is still decryptable by them from old git history they already have. Treat revocation as "rotate the sensitive values themselves" (new API keys, new database passwords), not as un-sharing. - Rollback when the attacker controls both the repo and your local anchor. The epoch anchor is per-machine local state; someone who can rewrite the repository and delete or rewind
~/.lock/epoch/on your machine (i.e. same-user local access) can serve you an old vault. A fresh clone on a brand-new machine has no anchor yet, so its first unlock accepts whatever epoch it sees. - A malicious provider binary. Dynamic providers execute as your user. Digest pinning detects replacement, not malice at install time.
- Weak master passwords. Argon2id slows brute force; it cannot rescue a guessable password.
Layer 2 — The actual cryptography
Exact primitives and parameters, for verification against the source. Nothing here is bespoke crypto — every construction is a standard composition of well-reviewed primitives (RustCrypto crates and crypto_box).
Password → master key. Argon2id v1.3 with 64 MiB memory (m = 65536 KiB), t = 3 iterations, p = 1 lane, over a random 16-byte per-vault salt, producing a 32-byte master key. The master key exists only in memory and is zeroized after use.
Master key → KEK (domain separation). HKDF-SHA256 with salt dotlock:v1:hkdf and info string dotlock:v1:kek:project=<uuid>:env=<name>:version=<n>. Binding the project UUID, environment name and key version into the derivation is what makes environments cryptographically isolated: the same password would still derive different KEKs per environment, and a wrapped blob from one environment or key version cannot be replayed in another.
All encryption and wrapping: XChaCha20-Poly1305 AEAD. Every encryption in the system — secret values, wrapped project key, wrapped per-secret keys, the integrity hash, the session cache — uses XChaCha20-Poly1305 with a fresh random 192-bit (24-byte) nonce per operation (the extended nonce makes random generation safe without counters) and context-binding additional authenticated data:
- each secret record's AEAD is bound to a length-prefixed encoding of
id | name | updated_at | version— so a valid ciphertext cannot be swapped under another variable name, and an old version of a secret cannot be replayed as the current one; - the wrapped project key's AAD binds
project,environmentandkek_version(with a one-shot fallback that still authenticates pre-versioning vaults).
Integrity of secrets.lock. After every write, DotLock stores the SHA-256 of secrets.lock encrypted under the project key (AAD dotlock:v1:secrets-hash) in vault.toml; on unlock it recomputes the file hash and compares in constant time. Because the stored hash is itself an AEAD ciphertext under the DEK, an attacker without the password cannot recompute a matching value after modifying the file.
Integrity of vault.toml. A metadata MAC: HMAC-SHA256 over a canonical encoding of the vault metadata, keyed by HKDF-SHA256 from the project key (salt dotlock:v1:metadata-mac, info bound to the project UUID — domain-separated from the KEK derivation). Verified in constant time on unlock. This covers the recipient list, epoch and key metadata, so none of it can be edited undetected.
Rollback protection. The MAC-covered vault epoch is a monotonic counter bumped on rotations and other security-relevant writes. Each machine persists the newest epoch it has seen per project under ~/.lock/epoch/ (outside the repo); an unlock that presents an older epoch than the anchor fails unless explicitly accepted with DOTLOCK_ALLOW_VAULT_ROLLBACK=1 (for legitimate checkouts of old revisions).
Sharing. Identities are Ed25519 (PKCS#8 PEM, optionally scrypt-encrypted). Grants, audit entries and the audit high-water mark are Ed25519 signatures. Key wrapping to a recipient is an X25519 sealed box (crypto_box, libsodium-compatible seal); the recipient's X25519 key is derived from their Ed25519 key via the standard Edwards→Montgomery map — the same construction libsodium and age use. RSA-3072 (OAEP/PSS) is kept only to read legacy material — existing RSA-wrapped entries and old RSA-signed grants/audit lines; no fresh setup ever executes RSA code, and dl cert migrate moves an identity and project off it entirely. Rationale, the RUSTSEC-2023-0071 ("Marvin") assessment and the migration design are in ADR 0001.
Audit log. JSONL entries, each carrying the SHA-256 hash of its predecessor (hash chain) and an Ed25519 signature when a usable identity is present. A signed high-water mark (count + head hash) is written alongside, so truncating the tail of an otherwise-valid chain is detected. dl audit verify is strict by default (unsigned entries fail); --lax downgrades them to warnings.
Session cache. After a successful unlock, the project key may be cached briefly so consecutive commands do not re-prompt (default TTL 15 s via DOTLOCK_CACHE_TTL, capped at 300; disabled in shared mode unless DOTLOCK_SHARED_CACHE is set). The cached key is never stored raw: it is XChaCha20-Poly1305-encrypted under a key derived from a separate per-user random key file (~/.lock/run/session.key, mode 0600), with the expiry timestamp bound as AAD. Expired or tampered entries are overwritten with zeros and deleted on sight; dl lock shreds them on demand. Honest residual: the wrapped cache and its wrapping key live on the same filesystem — a same-uid process within the TTL can recover the project key; the wrapping defeats single-file exposure (a backed-up sessions.toml, a log shipper), not a same-user attacker. Filesystem snapshots, swap and copy-on-write storage may also defeat best-effort shredding. On multi-tenant machines, set DOTLOCK_CACHE_TTL=0 or run dl lock after sensitive sessions.
File permissions. On Unix: private directories 0700, secret/private files 0600 (mode applied at open/mkdir time), public key files 0644, O_NOFOLLOW symlink-safe opens, and atomic temp-file-and-rename writes. Windows is covered below.
Windows
File-permission hardening is implemented on both families. On Windows, DotLock applies a restrictive DACL to every sensitive file and directory it creates (vault/secrets files, identities, session cache, audit log): a single access-control entry granting full control to the current user only, with inheritance from the parent severed (PROTECTED_DACL_SECURITY_INFORMATION), so no other account — including other members of local groups — retains access through inherited ACEs. Private directories get an inheritable owner-only ACE so files born inside them (e.g. transaction journals) start owner-only. If applying the DACL fails, the operation errors out rather than silently leaving a permissive file behind.
Honest residual differences on Windows: there is no O_NOFOLLOW equivalent in the portable open path, so symlink rejection falls back to a check-then-open pattern — a narrow race window that requires a local attacker who can already write inside your protected directories. Files that existed before this hardening keep whatever ACLs they had until rewritten, and Administrators can always take ownership of any file; that is inherent to the platform.
Operational notes
- Commit
.lock/vault.tomland.lock/secrets.lock(and.lock/envs/) if your team shares the encrypted vault through Git; never commit plaintext.envfiles after migrating. - Prefer
dl set NAME(hidden prompt) or--stdinover passing values in argv; preferdl runoverdl getfor consumption. - Keep local identity private keys protected; use
dl cert init --plainonly for controlled automation or ephemeral environments. - Known dependency advisories (RSA "Marvin" RUSTSEC-2023-0071 and monitored transitive crates) are documented in ADR 0001; CI enforces
cargo audit/cargo denyso only the documented advisories are accepted. Since the Ed25519/X25519 migration the RSA paths run only when reading legacy material — rundl cert migrateto leave them behind entirely.
Configuration
Project configuration
Stored in .lock/vault.toml, managed with dl config:
| Key | Default | Accepted values | Effect |
|---|---|---|---|
auto_fetch_on_run |
false |
true, false, 1, 0, yes, no, on, off |
Enables Git auto-fetch before dl run |
auto_fetch_timeout_secs |
3 |
positive integer | Timeout for Git auto-fetch operations |
auto_fetch_remote |
origin |
non-empty string | Remote used by auto-fetch and dl sync |
auto_ratchet_after_writes |
off | non-negative integer | Rotates key wrapping automatically after this many vault writes; 0 disables (also a dl rotate --if-due policy) |
rotate_max_age_days |
off | non-negative integer | Marks a project-key rotation as due for dl rotate --if-due after this many days; 0 disables |
dynamic_resolve_timeout_secs |
10 |
positive integer | Timeout for dynamic provider resolution |
Environment variables
| Variable | Default | Effect |
|---|---|---|
DOTLOCK_ENV |
unset | Selects the environment vault; overridden by --env, overrides the dl env use selection |
DOTLOCK_MASTER_PASSWORD |
unset | Non-interactive master password; overridden by --password-stdin / --password-file (preferred — env vars can leak in CI logs and process listings) |
DOTLOCK_CACHE_TTL |
15 |
Cached project key lifetime in seconds, capped at 300; 0 makes entries expire immediately (effectively disabling the cache) |
DOTLOCK_CACHE_DIR |
$HOME/.lock on Unix, %LOCALAPPDATA%\dotlock on Windows |
Overrides the session cache root |
DOTLOCK_SHARED_CACHE |
off | Set to 1, true, TRUE, yes or YES to enable caching in shared mode |
DOTLOCK_IDENTITY_DIR |
$HOME/.lock/identity |
Overrides local identity storage |
DOTLOCK_HOME |
unset | Overrides the per-user state root ($HOME/.lock / %LOCALAPPDATA%\dotlock); required in environments without HOME |
DOTLOCK_AUDIT_DIR |
$HOME/.lock/audit on Unix, %LOCALAPPDATA%\dotlock\audit on Windows |
Overrides audit log storage |
DOTLOCK_AUTO_FETCH |
unset | Set to 0, false, no or off to disable auto-fetch for one command |
DOTLOCK_ALLOW_VAULT_ROLLBACK |
unset | Set to 1 to accept a vault whose epoch is older than the newest one this machine has seen (e.g. a legitimate checkout of an older revision) |
Command Reference
| Command | Aliases | Purpose |
|---|---|---|
dl init |
i |
Initialize .lock/ in the current project |
dl set [--alg xcha] NAME [VALUE] [--stdin] |
s, add |
Store or update a static secret (hidden prompt when VALUE is omitted) |
dl set NAME --provider PROVIDER [--config JSON] [--bootstrap A,B] |
s |
Store a dynamic secret definition |
dl get NAME [--reveal] |
g |
Print a secret (masked on a TTY unless --reveal; piped output is always the bare value) |
dl unset NAME [--yes] |
u, rm, remove, d, del, delete |
Remove a secret (confirms; --yes/-y skips) |
dl list |
l |
List stored secrets without plaintext |
dl run [--env-file FILE] -- CMD [args...] |
r |
Run a command (argv form, no shell) with decrypted secrets in its environment |
dl exec [--env-file FILE] COMMAND... |
e |
Run a shell command line (sh -c) with decrypted secrets in its environment |
dl lock |
k, logout |
Shred the cached project key |
dl migrate [PATH] |
m, import |
Import variables from .env |
dl export [PATH] |
x |
Append missing static secrets to .env |
dl sync |
sy |
Fast-forward from the configured Git remote when safe |
dl cert init [--force] [--plain] |
crt i |
Create a local Ed25519 identity |
dl cert show |
crt sh |
Show local identity information |
dl cert migrate [--plain] |
crt m |
Migrate a legacy RSA identity to Ed25519/X25519 and rekey this project's recipient entry |
dl cert export-pub [PATH] |
crt x |
Print or write the public key |
dl share enable |
shr en |
Enable shared mode |
dl share grant --pubkey FILE --label LABEL [--allow A,B] |
shr gr |
Grant recipient access (signed) |
dl share allow QUERY [--list] [--add A,B] [--remove A,B] |
shr al |
Inspect or edit a recipient's per-secret ACL (removal rotates affected keys) |
dl share revoke QUERY [--yes] |
shr rev |
Revoke a recipient and rotate the project key (confirms; --yes skips) |
dl share list |
shr l |
List recipients |
dl rotate kek [--yes] |
rot k |
Rotate the project key and rewrap secret keys (historical name for project-key) |
dl rotate master-password [--yes] |
rot mp |
Change the master password (confirms; --yes skips) |
dl rotate project-key [--yes] |
rot pk |
Rotate the project key (confirms; --yes skips) |
dl rotate --if-due |
rot --if-due |
Rotate only when due per rotate_max_age_days / auto_ratchet_after_writes; exits 0 when nothing is due |
dl env list |
ev l |
List this project's environments |
dl env add NAME |
ev a |
Create an environment with its own vault pair |
dl env use NAME |
ev u |
Persist NAME as this checkout's default environment |
dl env remove NAME [--yes] |
ev rm |
PERMANENTLY delete an environment's vault pair (confirms; --yes skips) |
dl audit show [--verbose] [--since YYYY-MM-DD] [--action ACTION] |
a s |
Show audit entries |
dl audit verify [--lax] |
a v |
Verify audit hash chain and signatures (strict by default) |
dl audit path |
a p |
Print the audit log path |
dl audit rotate |
a r |
Rotate and gzip the current audit log |
dl git install-merge-driver |
gt i |
Configure the Git merge driver |
dl config show |
c sh |
Show project config |
dl config set KEY VALUE |
c s |
Set a project config value |
dl config unset KEY |
c u |
Reset a project config value |
dl provider list |
p l |
List provider binaries on PATH |
dl provider info NAME |
p i |
Show provider description |
dl reconcile |
rec |
Review and re-sign a vault combined by the Git merge driver |
dl repair [--dry-run] [--prune] [--yes] |
rep |
Diagnose and recover a vault whose integrity hash is out of sync |
Global flags, accepted by every command: --json (machine-readable output for list, get, share list, audit show, provider list), --password-stdin / --password-file FILE (non-interactive master password; see CI usage), and --env NAME (operate on a named environment's vault; see Multiple Environments).
License
Dual-licensed under your choice of:
- MIT: LICENSE-MIT
- Apache 2.0: LICENSE-APACHE