Secrets
Project secrets in 1Password, attached to a project by reference and never written to disk.
A project’s secrets (API keys, database URLs…) live in 1Password, and
Varlock hands them to the project when it runs. No
secret is written to a .env file or exported in the shell.
dot apply packages # installs Varlock
Where they live
Project secrets go in a 1Password vault kept for development, apart from
your logins. dot.toml names it:
[secrets]
vault = "Dev"
A project never stores a value: its .env.schema holds a reference, and
Varlock resolves it when the project runs. Which secrets a project needs,
and with what permissions, is the project’s own documentation.
Add a secret to a project
1. Create it with the least it needs
Give each secret only what its job requires, and one secret per job, so revoking one breaks nothing else. A job that only reads gets a read-only token; a test environment gets a test key. Creating it usually happens on the provider’s website; each provider’s guide has the exact steps (GitHub token).
2. Store it
dot secret add myapp-payments # name it <project>-<use>
It asks for the value without showing it (or reads it from a pipe:
pbpaste | dot secret add myapp-payments), stores it in the vault as an API
Credential tagged dotfiles, and prints its reference,
op://Dev/myapp-payments/credential. The value never becomes a command
argument or a file, so it’s not in your shell history or the process list.
It refuses a name that already exists.
dot secret list # names, dates and references; never values
dot secret update myapp-payments # a new value, same reference
3. Attach it to the project
A project commits a .env.schema: which variables exist, their types, and
where each value comes from. varlock init creates it, and its header loads
the 1Password plugin. Then, from the project’s folder:
dot secret attach myapp-payments PAYMENTS_KEY
This adds the variable as a reference, never a value:
# @plugin(@varlock/1password-plugin)
# @initOp(allowAppAuth=forEnv(dev))
# ---
APP_URL=http://localhost:3000
# @sensitive
PAYMENTS_KEY=op(op://Dev/myapp-payments/credential)
dot secret attach refuses a variable the schema already has, and a schema
without the plugin.
4. Run with it
varlock explain PAYMENTS_KEY # how one variable resolves, without its value
varlock load # validate the schema and show what resolves
varlock run -- pnpm dev # run a command with the values injected
A Next.js project uses Varlock’s integration instead
(@varlock/nextjs-integration), so next dev and next build load the
schema themselves. Local overrides (.env.local) belong in each project’s
.gitignore and never hold a secret: that’s what the schema is for.
At runtime
- Only that process gets the secrets.
varlock runresolves the values (1Password asks for Touch ID) and injects them into the command it starts; your shell never has them. - Sensitive values are redacted in output that’s piped, such as logs.
- Cache: resolved values can be cached in
~/.config/varlock/cache/, encrypted with a Secure Enclave key that needs Touch ID.
Telemetry
Varlock sends anonymous usage analytics unless they’re turned off. Here that’s a personal value:
[varlock]
telemetry = "disable" # or "enable"
DO_NOT_TRACK=1 also turns it off for a single run.
Where each secret lives
| Secret | Lives in | Reaches its user through |
|---|---|---|
| App secrets (API keys, DB URLs) | 1Password, development vault | Varlock, per process |
GitHub token for gh |
1Password | the gh shell plugin, per command |
Vercel token for vercel |
1Password | the vercel shell plugin, per command |
| A project’s Vercel OIDC token | nowhere: issued per run, 12 hours | Varlock, through the vercel plugin (Vercel) |
| SSH key | 1Password | the SSH agent |
dot.toml backup |
1Password | dot conf restore |
Further reading
- Varlock: introduction
- The
.env.schemaformat (@env-spec) - Varlock’s 1Password plugin
- 1Password secret references
- 1Password service accounts, for a job that runs unattended on a server