@earthlink/dotvault
dotvault — sync .env / .npmrc / .gitconfig and any config file across
machines and CI/CD. No AWS account required.
Install
npm i -g @earthlink/dotvault
# or as a project devDependency
npm i -D @earthlink/dotvault
Requires Node >= 20.
Upgrading from
@earthlink/env-sync? Theenv-synccommand keeps working as an alias — see Compatibility alias below.
Quick start
# 1. Create project-config.json (first time, if not already in repo)
dotvault init
# 2. Log in — stores tokens in OS keychain (Keychain / libsecret / Credential Manager)
dotvault login -e [email protected]
# 3. Pull all synced files to local disk
dotvault pull
# 4. (Optional) Inject secrets into a dev server without writing any file
dotvault run -- npm run dev
Common workflows
| Workflow | Commands |
|---|---|
| First-time setup | dotvault init → dotvault login → dotvault pull |
| Join existing project (config in repo) | dotvault login → dotvault pull |
| Local dev (no .env on disk) | dotvault run -- npm run dev |
| CI/CD (service token) | set DOTVAULT_TOKEN=est_… → dotvault pull --env production |
| Audit history | dotvault versions DATABASE_URL |
| Roll back a secret | dotvault rollback DATABASE_URL --to 4 |
| Add a file to sync | dotvault push .npmrc |
| Detect committed secrets | dotvault scan . |
| Share a key org-wide | dotvault global keys → dotvault global use <KEY> |
All commands
dotvault init # create project-config.json
dotvault login # email + password auth (Cognito)
dotvault pull # write .env / .npmrc etc. to local disk
dotvault run -- <cmd> # inject secrets into child process (no file written)
dotvault set KEY=value # set one or more secrets
dotvault delete KEY # delete a secret
dotvault import .env.production # bulk-import from a .env file
dotvault versions KEY # show version history of a key
dotvault rollback KEY --to N # restore a key to version N
dotvault list # list (env, file) inventory for this project
dotvault push .npmrc # add any file to the sync set
dotvault files # list synced files
dotvault scan . # detect committed secrets (offline)
dotvault global keys # list the org's global key-pool inventory
dotvault global set K=v [-E env] # set a global-pool value (omit -E for the common value)
dotvault global delete K [-E env] # delete a global-pool value
dotvault global use K [--as N] # bind a global key into this project's pull
dotvault global drop K # unbind a global key from this project
dotvault global status # show this project's active global-key bindings
dotvault project create --name X # create a project (headless — ADR-0018)
dotvault logout # remove tokens from keychain
dotvault whoami # report active identity (interactive user OR service token)
Run dotvault <command> --help for per-command flags and examples.
Init options
dotvault init [options]
| Option | Default | Description |
|---|---|---|
-n, --name <name> | basename(cwd) | Project name |
--api <url> | https://api.dotvault.io/v1 | API base URL |
-f, --force | — | Overwrite existing project-config.json |
Success output:
Created project-config.json (project=my-app). Next: dotvault login && dotvault pull
Pull options
dotvault pull --project my-app --env staging --file .env.local
| Option | Description |
|---|---|
-p, --project <name> | Override project name from project-config.json (with this flag the config file is optional — see below) |
-E, --env <env> | development | staging | production, or any custom env the project has declared (Settings → Environments, ADR-0024) |
-f, --file <file> | Pull only this file |
Success output (one line per file, then a summary; colors/progress bar on a TTY only):
✓ Wrote 12 keys to .env.local (project=my-app, env=development, file=env.local). Markers preserved.
✔ 1 file synced, 0 excluded, 12 keys total
Push a whole file
dotvault push .npmrc [--as config/prod.json] [--env staging]
push uploads an entire file (e.g. .npmrc, kubeconfig, a JSON
service-account key) as one synced artifact. A later plain dotvault pull materializes every pushed file to its destPath, creating any
missing parent directories. A file that cannot be written (no
permission, a directory in the way, …) is named in a warning and the
other files are still written.
dotvault run puts the pushed files in place only while the command
runs and removes them afterwards. If a file already exists at the
destPath with the same content (for example after a pull), it is
used as is and kept. If the local file is different, it is left
untouched and run prints a warning.
Text files only (UTF-8). push reads and stores the file as UTF-8
text. A file that is not valid UTF-8 — binary files (images, archives,
compiled blobs) and text saved in another encoding such as Latin-1 or
Shift_JIS — is rejected before upload with a validation error. Convert
the file to UTF-8, or base64-encode binary data into a text file first
if you must sync it.
Size limit: 256 KiB. The server rejects a file longer than 262,144
characters (FILE_TOO_LARGE). For plain ASCII text that is 256 KiB;
text with multi-byte characters can be larger in bytes. Pushed files
travel inline with every pull, so secrets, not large assets, are the
intended payload.
Scan for committed secrets
dotvault scan . # scan a path tree (offline, no auth)
scan walks the given paths (skipping node_modules), flags
high-confidence secrets (AWS keys, private-key blocks, GitHub PATs,
dotvault service tokens) and redacts the matched value in its output.
Exit code is 1 when any finding is reported, 6 when a named
top-level path does not exist (SCAN_PATH_NOT_FOUND), 0 when the
tree is clean.
Known limitation (generic assignments). The generic-assignment
rule only flags quoted literals of 16+ characters (e.g. password = "supersecretvalue123"). It deliberately does not flag "${VAR}"
interpolation references (GitHub #254) and, to avoid noise, does not
flag short or unquoted values such as a plain PASSWORD=value line in
a .env file. scan is a committed-secret tripwire, not an exhaustive
linter — treat a clean result as "no obvious high-entropy secrets",
not a guarantee.
Exit codes
| Exit | Meaning |
|---|---|
| 0 | Success |
| 1 | Bad request, or scan found possible secrets |
| 2 | Network error (NETWORK_ERROR) — the API could not be reached; safe to retry |
| 3 | Not logged in, session expired, or wrong credentials / MFA code |
| 4 | Permission denied (PERMISSION_DENIED) |
| 5 | Project or secrets not found on the server (PROJECT_NOT_FOUND) |
| 6 | Invalid input on this machine (VALIDATION_ERROR, SCAN_PATH_NOT_FOUND) |
| 7 | rotate conflict (409 from a rotation route, e.g. a RUN already in flight) |
| 99 | Unexpected error, including other server responses (e.g. 413, 429, 5xx) |
dotvault run exits with the child command's exit code (127 when the
command could not be started).
Project config
The CLI reads project-config.json in the working directory root (generated by dotvault init, schema 2.7.0).
With an explicit -p, --project <name> the file is optional (ADR-0035):
every project-scoped command (pull, run, list, set, push, files, …)
then works from any directory. Without a config the API URL falls back to
DOTVAULT_API_URL → the well-known default, the env falls back to
--env → DOTVAULT_ENV/APP_ENV → development, and write commands
(set etc.) require an explicit --file. Without --project the config
stays required — it is what anchors the directory to a project:
{
"name": "my-app",
"setup": {
"bootstrap": {
"proxy": {
"apiUrl": "https://api.dotvault.io/v1"
}
},
"envSync": {
"enabled": true,
"files": [".env"],
"defaultEnv": "development"
}
}
}
Commit this file to your repository — it contains no secrets. The apiUrl field is the only value that must match the hosted platform.
Authentication
Token resolution order for each CLI invocation (applies to pull, run, set, delete, import, versions, rollback, list, push, files, whoami):
DOTVAULT_TOKENenvironment variable (or legacyENV_SYNC_TOKEN) —est_…service tokens (CI/CD/Lambda)- OS keychain — set by
dotvault login(interactive user)
dotvault whoami will tell you which mode is active without making a network call:
$ DOTVAULT_TOKEN=est_ci_secret_abcdefghij dotvault whoami
Service token (DOTVAULT_TOKEN=est_ci_secre…) — CI / Lambda mode
$ dotvault whoami
[email protected] (session valid until 2027-10-24)
Service tokens — admin console only
Service tokens (est_…) are created, listed, and revoked in the admin console:
app.dotvault.io/admin/tokens. The secret
is shown once — save it immediately.
dotvault token list | create | revoke was removed in v0.15.6: the token API
only accepts admin-console sessions (a CLI login is rejected), so those
commands always failed with PERMISSION_DENIED. They now just print the
console URL and exit 1.
Headless project creation (dotvault project create)
Create a project without the admin web console — the piece that lets Claude
Code / CI onboard a repo unattended (ADR-0018). Mint a provisioner token once,
then create projects with no interactive login:
# one-time (org admin): create a `provisioner` token in the admin console
# (https://app.dotvault.io/admin/tokens) — it creates projects, cannot read or
# write any secret, and cannot mint tokens
# thereafter (headless): org is taken from the token, so --org is optional
DOTVAULT_ADMIN_TOKEN=est_… dotvault project create --name myapp
Credentials for project create: DOTVAULT_ADMIN_TOKEN (or legacy ENV_SYNC_ADMIN_TOKEN) →
DOTVAULT_TOKEN (or legacy ENV_SYNC_TOKEN). A dotvault login session cannot
create projects — without a provisioner token, create it in the admin console. See the
Claude Code onboarding runbook.
Compatibility alias (env-sync)
The package still installs an env-sync binary alongside dotvault — every
command works identically under either name. Invoking any command via
env-sync prints a one-line stderr notice:
env-sync is now dotvault — the env-sync command keeps working as an alias.
Legacy ENV_SYNC_* environment variables (ENV_SYNC_TOKEN,
ENV_SYNC_ADMIN_TOKEN, ENV_SYNC_API_URL) are still read as a fallback when
the corresponding DOTVAULT_* variable is unset — there is no forced
migration deadline.
Links
- Admin console: https://app.dotvault.io
- Help & FAQ: https://app.dotvault.io/admin/help
- Full documentation: https://github.com/EarthLinkNetwork/dotvault#readme
- Source: https://github.com/EarthLinkNetwork/dotvault
Access
The Lambda proxy backend is operated by EarthLink Network. Without a Cognito account in the operator's user pool you cannot fetch secrets — this package is intentionally distributable on npm but operationally private to its tenant.
License
MIT