Skip to content
Image
Image

Pass Plugin

Our pass plugin enables loading secrets from pass (the standard unix password manager) using declarative instructions within your .env files.

Pass stores each secret as a GPG-encrypted file in ~/.password-store, organized in a simple directory hierarchy. This plugin shells out to the pass CLI, so it works with your existing GPG agent, git-backed stores, and all standard pass configuration.

  • Zero-config: works with your existing pass store
  • GPG-backed encryption: uses pass’s native GPG security model
  • Auto-infer entry paths from variable names
  • Bulk-load secrets with passBulk() via @setValuesBulk
  • Multiple store instances for accessing different pass stores
  • Name prefixing for scoped entry access
  • allowMissing option for optional secrets
  • In-session caching: each entry is decrypted only once per resolution
  • Helpful error messages with resolution tips

In a JS/TS project, you may install the @varlock/pass-plugin package as a normal dependency. Otherwise you can just load it directly from your .env.schema file, as long as you add a version specifier. See the plugins guide for more instructions on installing plugins.

.env.schema
# 1. Load the plugin
# @plugin(@varlock/pass-plugin)
#
# 2. Initialize the plugin - no arguments needed for default setup
# @initPass()

You must have pass installed on your system:

Terminal window
# macOS
brew install pass
# Ubuntu/Debian
sudo apt-get install pass
# Arch
pacman -S pass

Your password store must already be initialized (pass init "Your GPG Key ID"). See the pass documentation for setup details.

If your password store is in a non-standard location, use storePath:

.env.schema
# @plugin(@varlock/pass-plugin)
# @initPass(storePath=/path/to/custom/store)

This sets PASSWORD_STORE_DIR for all pass commands issued by this plugin instance.

Use namePrefix to scope all entry lookups under a common prefix:

.env.schema
# @plugin(@varlock/pass-plugin)
# @initPass(namePrefix=production/app/)
# ---
# Fetches "production/app/DATABASE_PASSWORD" from the store
DATABASE_PASSWORD=pass()
# Fetches "production/app/stripe-key"
STRIPE_KEY=pass("stripe-key")

Access multiple different password stores (e.g., personal and team):

.env.schema
# @plugin(@varlock/pass-plugin)
# @initPass(id=personal)
# @initPass(id=team, storePath=/shared/team-store)
# ---
MY_TOKEN=pass(personal, "tokens/github")
SHARED_KEY=pass(team, "api-keys/stripe")

Once the plugin is installed and initialized, you can start adding config items that load values using the pass() resolver function.

Fetch secrets from your pass store:

.env.schema
# Entry path defaults to the variable name
DATABASE_PASSWORD=pass()
API_KEY=pass()
# Or explicitly specify the entry path
STRIPE_KEY=pass("services/stripe/live-key")
# Nested entries
DB_URL=pass("production/database/url")

When called without arguments, pass() uses the config item key as the entry path.

Use allowMissing when a secret may not exist in the store:

.env.schema
# Returns empty string instead of erroring if entry doesn't exist
OPTIONAL_KEY=pass("monitoring/datadog-key", allowMissing=true)

By default, pass() returns only the first line of the entry (the password), matching pass’s own convention where the password lives on line 1 and metadata follows. This is the same behavior as pass -c (copy to clipboard).

To retrieve the full multiline content, use multiline=true:

.env.schema
# Only returns the first line (the password)
DB_PASSWORD=pass("production/database")
# Returns all lines (password + metadata)
DB_FULL_ENTRY=pass("production/database", multiline=true)

Use passBulk() with @setValuesBulk to fetch all entries under a directory in your pass store in one go, instead of wiring up each secret individually:

.env.schema
# @plugin(@varlock/pass-plugin)
# @initPass()
# @setValuesBulk(passBulk("services"))
# ---
# These will be populated from entries under services/ in the pass store
STRIPE_KEY=
DATABASE_URL=

passBulk() lists entries via pass ls, then fetches each one in parallel. Each entry returns the first line only (matching the pass() default).

You can customize the scope:

.env.schema
# Load everything from the store root
# @setValuesBulk(passBulk())
# Load from a specific subdirectory
# @setValuesBulk(passBulk("production/api"))
# With a named instance
# @setValuesBulk(passBulk(team, "shared"))

Initialize a pass plugin instance for accessing secrets from a password store.

Key/value args:

  • storePath (optional): Custom password store path (overrides PASSWORD_STORE_DIR, defaults to ~/.password-store)
  • namePrefix (optional): Prefix automatically prepended to all entry paths
  • id (optional): Instance identifier for multiple instances
# Default setup
# @initPass()
# Custom store location
# @initPass(storePath=/path/to/store)
# With prefix and ID
# @initPass(id=prod, namePrefix=production/)

Fetch a secret from the pass store. Returns the first line of the entry (the password) by default, matching pass’s convention.

Array args:

  • instanceId (optional): instance identifier to use when multiple plugin instances are initialized
  • entryPath (optional): path to the entry in the pass store. If omitted, uses the variable name.

Key/value args:

  • allowMissing (optional): if true, returns empty string instead of erroring when the entry doesn’t exist
  • multiline (optional): if true, returns the full entry content instead of just the first line
# Auto-infer entry path from variable name
DATABASE_PASSWORD=pass()
# Explicit entry path
STRIPE_KEY=pass("services/stripe/live-key")
# With instance ID
TEAM_SECRET=pass(team, "shared/api-key")
# Allow missing entries
OPTIONAL=pass("maybe/exists", allowMissing=true)
# Get full multiline content
FULL_ENTRY=pass("services/config", multiline=true)

Fetch all entries under a directory in the pass store at once. Intended for use with @setValuesBulk.

Lists entries via pass ls, then fetches each one in parallel. Each entry returns the first line only (matching the pass() default).

Array args:

  • instanceId (optional): instance identifier to use when multiple plugin instances are initialized
  • pathPrefix (optional): directory prefix to load entries from
# Load all entries from the store root
# @setValuesBulk(passBulk())
# Load entries under a specific path
# @setValuesBulk(passBulk("services"))
# With instance ID
# @setValuesBulk(passBulk(team, "shared"))

.env.schema
# @plugin(@varlock/pass-plugin)
# @initPass()
# ---
# Entry paths match variable names
DATABASE_URL=pass()
REDIS_URL=pass()
STRIPE_KEY=pass()
.env.schema
# @plugin(@varlock/pass-plugin)
# @initPass(namePrefix=production/)
# ---
# Fetches production/database/url, production/database/password, etc.
DB_URL=pass("database/url")
DB_PASSWORD=pass("database/password")
STRIPE_KEY=pass("api/stripe-key")
SENDGRID_KEY=pass("api/sendgrid-key")
.env.schema
# @plugin(@varlock/pass-plugin)
# @initPass(id=personal)
# @initPass(id=team, storePath=/shared/team-pass-store)
# ---
# Personal dev tokens
GH_TOKEN=pass(personal, "tokens/github")
# Shared team secrets
SHARED_DB=pass(team, "databases/staging")
SHARED_API_KEY=pass(team, "api-keys/internal")

  • Install pass using your system package manager (see Prerequisites)
  • Ensure pass is in your PATH
  • Verify the entry exists: pass show <path>
  • List available entries: pass ls
  • Check for typos in the entry path
  • If using namePrefix, remember it’s prepended automatically
  • Ensure your GPG key is available: gpg --list-keys
  • Start the GPG agent: gpgconf --launch gpg-agent
  • You may need to enter your GPG passphrase
  • Run pass init "Your GPG Key ID" to initialize the store
  • See pass init --help for details