Skip to content

Shell Plugins

The dotsecenv shell plugins automatically load secrets from .secenv files when you cd into a directory. When you leave, they are automatically unloaded—keeping your environment clean and secure.

  1. On cd into a directory: The plugin checks for a .secenv file
  2. Security check: The file must be owned by you (or root) and not world-writable
  3. Trust prompt: You’re prompted to allow loading (can be remembered)
  4. Auto-load: Plain variables are loaded first, then secrets are fetched from your vault
  5. Tree-scoped persistence: Secrets remain loaded in subdirectories, only unloading when you leave the tree entirely
~/project $ cd myapp/
dotsecenv: found .secenv in /home/user/myapp
Load secrets? [y]es / [n]o / [a]lways: y
dotsecenv: loaded 2 secret(s) from .secenv: DATABASE_URL, API_KEY
~/project/myapp $ cd src/ # Secrets persist in subdirectories
~/project/myapp/src $ cd ../.. # Leave the tree
dotsecenv: unloaded 2 secret(s): DATABASE_URL, API_KEY

Secrets are tree-scoped, meaning they persist while you navigate within subdirectories:

~/project $ cd myapp/
dotsecenv: loaded 2 secret(s) from .secenv: DATABASE_URL, API_KEY
~/project/myapp $ cd src/components/
# Secrets still available - we're in the myapp tree
~/project/myapp/src/components $ echo $DATABASE_URL
postgres://...
~/project/myapp/src/components $ cd ~/other-project/
dotsecenv: unloaded 2 secret(s): DATABASE_URL, API_KEY
# Secrets unloaded - we left the myapp tree

This behavior is ideal for common workflows where you navigate between source files while keeping your secrets loaded.

There are two workflows depending on how you reach your working directory:

Option 1: Navigate through the parent directory

If you cd into the directory containing .secenv first, secrets auto-load and persist as you navigate deeper:

~ $ cd myapp/ # .secenv loads here
dotsecenv: loaded 2 secret(s) from .secenv: DATABASE_URL, API_KEY
~/myapp $ cd terraform/ # Secrets persist — still in the myapp tree
~/myapp/terraform $ echo $DATABASE_URL
postgres://... # Available!

Option 2: Jump directly to a subdirectory, then run dse up

If you open a terminal directly in a subdirectory (or jump there with cd), the parent’s .secenv was never traversed, so secrets aren’t loaded. Use dse up to walk up the directory tree and load ancestor .secenv files:

~ $ cd myapp/terraform/ # Jumped directly — parent .secenv not loaded
~/myapp/terraform $ echo $DATABASE_URL
# Empty — secrets not loaded
~/myapp/terraform $ dse up # Walk up, find and load ancestor .secenv files
dotsecenv: loaded 2 secret(s) from .secenv: DATABASE_URL, API_KEY
~/myapp/terraform $ echo $DATABASE_URL
postgres://... # Now available!

If a subdirectory has its own .secenv, secrets layer on top of the parent:

~/project $ cd myapp/
dotsecenv: loaded 1 secret(s) from .secenv: DATABASE_URL
~/project/myapp $ cd prod/
dotsecenv: loaded 1 secret(s) from .secenv: API_KEY
# Both DATABASE_URL (from parent) and API_KEY (from prod) are available
~/project/myapp/prod $ cd ..
dotsecenv: unloaded 1 secret(s): API_KEY
# prod's API_KEY unloaded, parent's DATABASE_URL remains

If a child directory defines the same variable as the parent, the child’s value takes precedence. When you leave the child, the parent’s value is restored (re-fetched from the vault for security).

When you return to a directory with .secenv, secrets are reloaded fresh from your vault:

~/project/myapp $ cd ~/other-project/
dotsecenv: unloaded 2 secret(s): DATABASE_URL, API_KEY
~/other-project $ cd ~/project/myapp/
dotsecenv: loaded 2 secret(s) from .secenv: DATABASE_URL, API_KEY
# Fresh reload ensures you get the latest values

Oh-My-Zsh:

Terminal window
git clone https://github.com/dotsecenv/plugin.git \
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/dotsecenv

Then add dotsecenv to your plugins in ~/.zshrc:

Terminal window
plugins=(... dotsecenv)

Antigen:

Terminal window
antigen bundle dotsecenv/plugin

Manual:

Terminal window
source /path/to/dotsecenv.plugin.zsh

The plugins provide the dse command with subcommands for common operations:

CommandDescription
dseShorthand for dotsecenv (pass-through)
dse get NAMEShorthand for dotsecenv secret get NAME
dse cp NAMECopy a secret directly to clipboard
dse reloadReload secrets in the current directory
dse up [DIR]Load ancestor .secenv files above current directory

Use dse as a shorthand for the dotsecenv command:

Terminal window
# Instead of:
dotsecenv vault describe
# Use:
dse vault describe

Retrieve a secret with minimal typing:

Terminal window
# Instead of:
dotsecenv secret get DATABASE_PASSWORD
# Use:
dse get DATABASE_PASSWORD

Copy a secret directly to your clipboard without displaying it:

Terminal window
dse cp API_KEY
# Output: dotsecenv: secret copied to clipboard

Clipboard support:

  • macOS: Uses pbcopy (built-in)
  • Wayland: Uses wl-copy
  • X11: Uses xclip or xsel

Re-trigger secret loading for the current directory without leaving and re-entering:

Terminal window
dse reload

This is useful when:

  • You’ve added new secrets to your vault
  • You’ve modified the .secenv file
  • A secret fetch failed and you want to retry

When you jump directly into a subdirectory, parent .secenv files aren’t loaded because the plugin only auto-loads on cd into the exact directory containing the file. dse up fixes this by walking up the directory tree, finding all ancestor .secenv files, and loading them root-first:

Terminal window
dse up

The command:

  1. Walks up from the current directory to the git root (or filesystem root if not in a git repo)
  2. Collects all directories containing .secenv files
  3. Loads them root-first (topmost ancestor first), preserving correct layering
  4. Skips any ancestors whose secrets are already loaded

You can specify a custom boundary directory to limit how far up the walk goes:

Terminal window
dse up /home/user/projects # Stop walking at this directory

The plugin uses a trust system to prevent malicious .secenv files from auto-loading secrets:

When you cd into a directory with an untrusted .secenv:

dotsecenv: found .secenv in /path/to/project
Load secrets? [y]es / [n]o / [a]lways:
OptionBehavior
y / yesLoad secrets for this session only
n / noSkip loading for this session
a / alwaysPermanently trust this directory

Directories trusted with “always” are saved to:

~/.config/dotsecenv/trusted_dirs

To remove trust:

Terminal window
# Edit the file and remove the directory path
vim ~/.config/dotsecenv/trusted_dirs

Before loading any file, the plugin performs security checks:

  1. Ownership: File must be owned by you or root
  2. Permissions: File must not be world-writable

If a file fails these checks:

dotsecenv: refusing to load /path/.secenv - not owned by current user or root
dotsecenv: refusing to load /path/.secenv - world-writable

The .secenv file uses a simple KEY=value format with the {dotsecenv} syntax for loading secrets:

Terminal window
# Load secret with matching name
DATABASE_PASSWORD={dotsecenv}
# Load secret with different name
DB_PASS={dotsecenv/DATABASE_PASSWORD}
# Plain values work too
NODE_ENV=production
# Load from a namespace
API_KEY={dotsecenv/production::API_KEY}

See Your First Secret for more details on file syntax.

The dotsecenv plugin only handles .secenv files. This is intentional—we don’t reinvent the wheel.

Tools like direnv, autoenv, and zsh-autoenv have been managing .env files for years. They’re mature, battle-tested, and do their job well. There’s no reason for dotsecenv to duplicate that functionality or force you to learn subtle differences in behavior.

Use them together:

FileToolPurpose
.envdirenv, autoenv, etc.Non-sensitive configuration
.secenvdotsecenvSecrets from your vault

When loading a .secenv file:

  1. Phase 1: Load plain variables (any KEY=value without {dotsecenv})
  2. Phase 2: Fetch and load secrets (any KEY={dotsecenv} or KEY= {dotsecenv / name})

This ensures plain variables are available immediately, while secrets are fetched from your vault.

  1. Check the plugin is loaded:

    Terminal window
    type dse # Should show it's a function
  2. Verify .secenv exists and has correct permissions:

    Terminal window
    ls -la .secenv
  3. Try manual reload:

    Terminal window
    dse reload

The trust prompt requires an interactive terminal. This happens when:

  • Running in a script
  • Running in a non-interactive shell

Solution: Pre-trust the directory:

Terminal window
echo "/path/to/project" >> ~/.config/dotsecenv/trusted_dirs

If a secret fails to fetch, you’ll see:

dotsecenv: error fetching secret 'SECRET_NAME' for VAR:
<error details>

Common causes:

  • Secret doesn’t exist in vault
  • GPG agent not running
  • Vault locked