dotenvsec init is non-interactive. It cannot infer which environment variable
names belong in a scope or which public recipients should encrypt them. A bare
dotenvsec init therefore returns at least one --name and --recipient are required.
This guide uses the compact form:
iis an alias forinit.-n/--nameaccepts a comma-separated list or may be repeated.-r/--recipientacceptsID=PUBLIC_RECIPIENTand may be repeated.-p,-s, and-oare shorthand for--plugin,--sops, and--owner.
Do not interchange these executables or keys:
sopsencrypts and edits.env.sops.yaml. Its absolute path and SHA-256 are pinned in the repository's.dotenv-sec.yaml.dotenvsec-provider-sopsimplements the restricted dotenvsec provider protocol. Register it locally under provider IDsops.- An age or age-plugin identity decrypts the file. Commit only public recipients; keep private identity material in a private local file.
Install dotenvsec and SOPS as described in Installation. The Homebrew formula installs both dotenvsec binaries and SOPS:
brew install philband/tap/dotenvsecInstall the age implementation or hardware plugin that owns your identities. For YubiKey and Apple Secure Enclave requirements, read Hardware enrollment.
Official archives and the Homebrew formula install dotenvsec-provider-sops
beside the main dotenvsec binary. On every CLI invocation, dotenvsec locates
that canonical sibling, verifies its ownership and permissions, calculates its
SHA-256, and creates or refreshes the local sops provider entry automatically:
dotenvsec provider listThe entry should report ok and source: bundled in local settings. Automatic
refresh applies only to the official sibling named dotenvsec-provider-sops.
External/custom providers remain explicit and require provider register with
an absolute path and reviewed checksum.
SOPS accepts one consolidated age identity file. It may contain standard age
identities and supported plugin identity stanzas. The file must be absolute,
regular, non-symlinked, and accessible only by its owner (0600).
Local settings are stored at:
- macOS:
~/Library/Application Support/dotenvsec/settings.yaml - Linux:
${XDG_CONFIG_HOME:-$HOME/.config}/dotenvsec/settings.yaml
For standard age or Secure Enclave identities, add the private consolidated file path without removing generated settings:
schema: 1
identity_paths:
- /absolute/path/to/consolidated-age-identities.txt
editor: code --wait --reuse-window
providers:
sops:
executable: /absolute/path/to/dotenvsec-provider-sops
sha256: <provider-sha256>Never commit local settings or private identities.
YubiKey identities are managed automatically. On the first edit, exec,
shell, or hook activation that needs decryption, dotenvsec:
- Runs the trusted
age-plugin-yubikey -iexecutable from the repository's approved plugin path. - Matches connected identity metadata against active public recipients in the repository manifest.
- Atomically adds newly discovered matching identity references to a private managed identity file and configures its absolute path in local settings.
- Gives SOPS a temporary mode-
0600identity file containing non-YubiKey identities plus only the matching YubiKeys connected for that operation. - Deletes the temporary file immediately after SOPS exits.
Connect one locally authorized YubiKey before the first decryption. Connect a
different authorized key on a later operation to enroll it automatically. No
manual age-plugin-yubikey -i command is needed. Discovery does not request a
PIN or touch; SOPS prompts only when it decrypts with a connected matching key.
If no connected key matches an active recipient, dotenvsec fails before SOPS
instead of prompting for absent devices.
dotenvsec supports three storage modes:
- Repository (default): tracked scope files inside a Git worktree.
- Local (
--local): private untracked files in the selected directory; Git is optional. - Git-local (
--git-local): private untracked files in the main worktree, inherited read-only at the same relative path by every linked worktree.
Read Scope modes and worktrees before choosing local storage. Repository mode is the safest default for shared, reviewed configuration.
Run initialization inside an existing Git worktree. For one standard age recipient:
RECIPIENT="$(age-keygen -y "$HOME/.config/sops/age/keys.txt")"
dotenvsec i \
-n API_TOKEN,DATABASE_URL \
-r "primary=$RECIPIENT" \
-s "$(command -v sops)"For multiple YubiKeys, pair every stable device ID and public recipient in one repeatable flag:
dotenvsec i \
-n TF_HTTP_USERNAME,TF_HTTP_PASSWORD,TF_ENCRYPTION \
-s "$(command -v sops)" \
-p yubikey \
-r 'pb-yk-main=age1yubikey1...' \
-r 'pb-yk-dr-red=age1yubikey1...' \
-r 'dm-main=age1yubikey1...' \
-r 'dm-dr=age1yubikey1...'The owner and plugin flags apply to every recipient in that invocation. IDs and
public recipients must both be unique. Initialization encrypts to public
recipients and should not request a YubiKey PIN or touch. For hardware plugins,
initialization locates the corresponding age-plugin-* executable in PATH and
records its directory in the repository's checksum-approved provider policy.
Use local mode for a scope that must stay on one filesystem and must not be tracked. It also works outside Git:
dotenvsec i --local \
-n API_TOKEN,DATABASE_URL \
-r "primary=$RECIPIENT" \
-s "$(command -v sops)"Inside a Git worktree, initialization adds exact entries to the worktree's Git exclude mechanism so all four files remain ignored. dotenvsec subsequently rejects a local file that is not ignored, is force-added to Git, has group/world permissions, or is a symlink. Outside Git, the selected local directory is the scope boundary and moving it requires a new local approval.
Run Git-local initialization only from the main worktree:
dotenvsec i --git-local \
-n API_TOKEN,DATABASE_URL \
-r "primary=$RECIPIENT" \
-s "$(command -v sops)"The four private files remain in that main-worktree directory. A private
registry at $GIT_COMMON_DIR/dotenvsec/registry.yaml records only the owner
worktree and relative scope paths. Linked worktrees resolve the same encrypted
scope at the corresponding relative path without copying or symlinking files.
They may run status, doctor, exec, shell, and hook activation, but
allow, edit, and rekey must run from the main worktree. status reports
the inherited owner path.
Initialization is transactional. It creates all four files or none:
.dotenv-sec.yaml— scope policy and checksum-pinned SOPS path.dotenv-sec/recipients.yaml— public recipient governance.sops.yaml— creation rule generated from active recipients.env.sops.yaml— encrypted environment withCHANGE_MEplaceholders
It never writes plaintext environment values to the scope directory.
Inspect the policy and public metadata before trusting it:
git diff --no-index /dev/null .dotenv-sec.yaml || true
git diff --no-index /dev/null .dotenv-sec/recipients.yaml || true
git diff --no-index /dev/null .sops.yaml || trueFor repository mode, stage all four generated files. dotenvsec allow
deliberately rejects untracked repository scope inputs:
git add .dotenv-sec.yaml .dotenv-sec/recipients.yaml .sops.yaml .env.sops.yaml
dotenvsec allowFor local and Git-local mode, do not stage anything. Confirm the files are ignored/private, review them from the owner directory, and approve:
git status --ignored --short # when inside Git
dotenvsec status
dotenvsec allowGit-local approval is shared by linked worktrees because they represent the same repository and relative scope. Linked worktrees cannot create or replace that approval; review and approve only in the main worktree.
Approval stores a local trust hash in settings.yaml; it does not modify the
repository. Changes to recipients, declared variable names, provider checksum,
or scope policy invalidate that approval and require another review and
dotenvsec allow.
Choose the editor once in local user settings. From a VS Code integrated
terminal, the vscode preset opens the temporary decrypted document in the
current VS Code window and waits until that tab is closed:
dotenvsec settings editor vscodeThe preset stores code --wait --reuse-window. Any other SOPS-compatible editor
command can be selected globally by passing it as one quoted argument:
dotenvsec settings editor 'nvim --nofork'Show the current default with dotenvsec settings editor. Clear it and return
to the ambient SOPS_EDITOR or EDITOR environment with:
dotenvsec settings editor --clearEditor selection is local per user and is never controlled by repository files.
Open the encrypted document through the pinned SOPS executable:
dotenvsec editReplace every CHANGE_ME, save, and then verify the complete setup:
dotenvsec doctor
dotenvsec exec -- sh -c 'test -n "$API_TOKEN"'Use a test that checks presence without printing secret values. Commit only the encrypted/configuration files after review.
exec launches a real executable directly; shell aliases and functions are not
available. For example, if tf is an alias for OpenTofu, invoke the actual
binary:
dotenvsec exec -- tofu planThe -- separator is optional for simple commands but recommended so command
arguments beginning with - are unambiguous. Use dotenvsec shell when an
interactive shell with its aliases and functions is specifically required.
For interactive use, prefer an isolated child shell:
dotenvsec shellInstall the automatic zsh or bash hook only after doctor and child-only
loading work. Add the matching command to ~/.zshrc or ~/.bashrc:
eval "$(dotenvsec hook zsh)"The global hook is silent in directories without a configured scope, whether or not they are inside Git. Entering an unconfigured directory still clears any environment values managed by the previous scope. Errors from a discovered but broken or unapproved scope remain visible.
Schema 1 stored sops_executable, sops_sha256, and plugin_path in the
tracked scope file. That made one committed file unable to describe more than a
single machine, and turned every SOPS upgrade into a tracked-file edit that broke
the scope for everyone else. Schema 2 keeps only portable policy in the scope
file and holds the binding in the local provider registry.
Run once per scope, from the main worktree:
dotenvsec migrate
git diff .dotenv-sec.yamlmigrate binds the recorded SOPS executable and plugin path into local settings,
rewrites the scope file as schema 2, and preserves portable keys such as
sops_min_version. It refuses to run twice.
In repository mode, review and commit the rewritten scope file. Then, on every machine that uses the scope, bind the local tool and re-approve — the trust hash changes, and the binding is per-machine by design:
dotenvsec provider retool sops sops "$(command -v sops)"
dotenvsec allow
dotenvsec doctorColleagues on other platforms now bind their own paths without touching the
repository. If a machine has no SOPS recorded to migrate, migrate says so; bind
it with provider retool first.
init is non-interactive. Pass -n NAME and at least one
-r ID=PUBLIC_RECIPIENT.
Older dotenvsec releases encrypted stdin without telling SOPS that the intended
filename was .env.sops.yaml. They could leave three partial files and no
ciphertext. Upgrade dotenvsec, verify those files are untracked failed-init
output, remove only .dotenv-sec, .dotenv-sec.yaml, and .sops.yaml, then
retry. Current initialization supplies the filename and rolls back on failure.
The official provider sibling is missing or dotenvsec is running from a custom
layout. Reinstall the official archive/formula and ensure dotenvsec and
dotenvsec-provider-sops share one canonical directory. Explicit registration
is reserved for external providers; do not register the sops binary itself as
a provider.
Review and stage all four generated files before dotenvsec allow or other
trusted operations.
Local and Git-local files inside Git must be ignored and untracked. Do not force
add them. Initialization normally writes exact entries to Git's local exclude
file; inspect .git/info/exclude through the main repository's common Git
directory if an entry was removed.
The selected Git-local scope belongs to the main worktree. Run allow, edit,
or rekey from the owner path printed by dotenvsec status. Read/decrypt
operations remain available from the linked worktree.
Review the tracked policy and run dotenvsec allow. Policy changes invalidate
the previous local approval.
The repository pins the SOPS executable used during initialization. Review the package upgrade, update the pinned path/checksum through a trusted process, and approve the changed policy again. Never bypass the mismatch.
Homebrew removes the old versioned Cellar directory after upgrading dotenvsec. The next dotenvsec invocation automatically discovers and checksum-pins the new bundled provider. Since the trusted provider checksum changed, each repository's approval becomes invalid until it is reviewed again:
dotenvsec provider list
cd /path/to/repository
dotenvsec status
dotenvsec allowRe-approval is intentional: the locally trusted executable and its checksum
changed. Provider refresh is automatic; do not automate allow as part of a
package upgrade.
Check that identity_paths points to the correct consolidated private identity
file, its mode is 0600, and the required hardware plugin is installed. For
standard age or Secure Enclave identities, configure the private file explicitly.
For YubiKeys, connect a device whose public recipient is active in the repository;
dotenvsec discovers and enrolls its identity reference automatically. Decryption
may then require the configured PIN and touch policy.
Provider-based loading uses a restricted PATH. Run dotenvsec doctor; it
checks every age plugin required by active recipients, resolving them exactly as
the loading path does.
The search path is local machine state, so widening it needs no scope edit, no commit, and no re-approval:
dotenvsec provider retool sops sops "$(command -v sops)" \
--plugin-path /Users/example/.cargo/bin:/opt/homebrew/bininit detects the directory automatically when the plugin is already installed.
The default search path covers /opt/homebrew/bin on macOS plus the standard
system directories; Homebrew on Linux (/home/linuxbrew/.linuxbrew/bin) and
other non-standard prefixes must be added explicitly.
Releases before this one skipped any plugin installed as a symlink, which is how
Homebrew publishes every binary, so a Homebrew age-plugin-yubikey was reported
as missing while doctor still passed. Symlinks are now resolved and the
canonical target is checked.
Initialization never overwrites a scope. If a valid scope exists, use edit,
update its recipient manifest, and rekey. Remove files only when you have
confirmed they are untracked partial output from a failed initialization.