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.

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

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

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