Using 1Password and direnv to store developer secrets


This is more notes to myself so i don’t forget. Goals:

  • no secrets stored on the filesystem (so they can’t be sucked up by credential-harvesting malware)
  • individual vault per project
  • secrets automatically injected to envvars so that apps can follow 12-Factor App methodology
  • deployed secrets use platform native secret manager (e.g. Cloudflare secrets manager, Azure keyvault, AWS SSM Parameter Store etc.)
  • separation of this vault from my main password vault in BitWarden. My whole world exists in Bitwarden – i dont want to inadvertently expose it. And as Bitwarden has a pretty awful CLI experience, it doesnt work for me.
  • Safe to commit secret file to source control
  • Safe to commit my .env files to source control because they no longer contain secrets

Benefits of 1Password:

  • tightly integrated CLI tool and desktop app, with MacOS biometric auth integration
  • Industry standard toolset, well regarded security position
  • relatively cheap – roughly $50 per year

Table of Contents

Initial set up

1. Install 1Password, 1password-cli, direnv.

1Password must be the direct download or Homebrew cask version, not the Mac App Store version as App Store enforces sandboxing.

brew install --cask 1password 1password-cli
brew install direnv

2. Configure 1Password desktop app

  • Settings > Developer > Turn on “Show 1Password Developer experience”
  • Settings > Developer > Turn on “Integrate with 1Password CLI”
  • Settings > Security > Turn on “Unlock with Touch ID”

3. Configure shell

Add to ~/.zshrc. This causes direnv to evaluate a .env.tpl file each time you navigate in to a folder, and clear when you navigate out of a folder.

eval "$(direnv hook zsh)"

Open a new terminal shell (or type it in the existing one).

Adding secrets to a project

1. Add the secret to 1Password

Create a new vault in 1Password

Vaults are the security separation between projects. There is no hard limit to the number of vaults you can add.

Create a new item per environment

I create a new “password” type per environment. You can then add config as additional “password” types, with the name of the envvar, for example:

Screenshot of a 1Password "New Item" dialog creating a login entry titled "dev." Fields shown: username (empty), password (empty), a custom field labeled "secret_name" with a masked value, notes (empty), and options to add a website, location, or tags. The item is owned by user "Rob" in a vault called "project." A blue Save button is in the bottom-right corner.

2. Create .env.tpl in the project folder

This file is safe to commit – it contains no secrets, only references to secrets – example:

export MY_SECRET="op://project/dev/secret_name"

3. Create .envrc in project folder

Also safe to commit. This is what direnv runs on entry. Typically:

eval "$(op inject -i .env.tpl)"

One thing to note: if the desktop app is locked and the biometric prompt/login is declined (or times out), the env vars get set to empty strings, not the op:// references.

4. Allow direnv

A security measure…

direnv allow

5. Test

Exit the directory and go back in.

❯ cd .. && cd -
~/Downloads/myfolder
direnv: loading ~/Downloads/myfolder/.envrc
direnv: export +MY_SECRET

You should see a prompt from 1Password to grant access to the vault. This happens the first time after vault timeout/lock.

1Password authorization prompt requesting CLI access for iTerm2. The dialog shows the iTerm2 terminal icon connected via a green checkmark to the 1Password icon, with the text "Allow iTerm2 to get CLI access." The account "Rob" is listed below with an expandable disclosure arrow. Cancel and blue Authorize buttons at the bottom.

Use with VSCode

Install the official mkhl.direnv extension (source). This will read .envrc natively whenever a folder is opened. There’s a direnv: Reload command (Cmd+Shift+P).

Updating or adding new secrets

If new, add the op:// reference to .env.tpl. Update the secret in 1Password, run direnv reload in the folder.

Deploying and using secrets in other places

Platform-native secret stores inject env vars. App code unchanged.

  • CI – either:
    • Deploy secrets as Github Actions secrets, then mapped to envvars in the workflow
    • or use the 1Password Github Action with a Service Account to pull directly from the vault
  • Cloudflare: wrangler secret put SECRET_NAME → `env.SECRET_NAME`
  • Azure: Key Vault + managed identity → App Settings with @Microsoft.KeyVault(SecretUri=…)
  • AWS: SSM Parameter Store (Standard, free) → env vars in Lambda/ECS task definitions
  • Local containers: op run --env-file=.env.tpl -- docker compose up

Leave a Reply

Your email address will not be published. Required fields are marked *