Store environment variables secretly
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ralph Schaer c5bc3b9166 upgrade
2026-09-15 06:53:39 +02:00
.github/workflows upgrade 2026-07-27 05:52:45 +02:00
cmd Improve vault safety and CLI workflows 2026-09-04 19:47:47 +02:00
internal Improve vault safety and CLI workflows 2026-09-04 19:47:47 +02:00
.gitignore Initial commit 2026-04-07 21:43:55 +02:00
.golangci.yml upgrade 2026-05-31 15:50:52 +02:00
.goreleaser.yaml feat(tui): implement TUI model for managing vault entities 2026-04-10 20:37:03 +02:00
go.mod upgrade 2026-09-15 06:53:39 +02:00
go.sum upgrade 2026-09-15 06:53:39 +02:00
LICENSE update 2026-07-20 19:44:32 +02:00
README.md Improve vault safety and CLI workflows 2026-09-04 19:47:47 +02:00
Taskfile.yml upgrade 2026-09-04 15:58:45 +02:00

env-vault

Store named sets of environment variables in a local encrypted vault and inject them directly into processes — no plaintext files, no shell history exposure.

Install

Build from source:

git clone https://github.com/ralscha/env-vault
cd env-vault
go build -o env-vault ./cmd/cli
go build -o env-vault-tui ./cmd/tui

The CLI and TUI ship as separate binaries. Use env-vault for scripting and env-vault-tui for interactive management.

TUI

The TUI is optimized for day-to-day vault management with an explorer-like layout:

  • Searchable entity list on the left for apps and groups.
  • Detail pane on the right with direct keys, linked groups, and a resolved preview.
  • Modal forms for creating entities, editing keys, linking groups, renaming, copying, and removing entries.

Run it with the same vault-location and password flags as the CLI:

env-vault-tui
env-vault-tui --dir ~/.config/my-vault
printf '%s' "$ENV_VAULT_PASSWORD" | env-vault-tui --password-stdin

Inside the TUI:

  • / filters by entity name, kind, key name, or linked group.
  • n creates an app or group.
  • a adds or updates a direct key.
  • d removes a direct key.
  • l and u link or unlink groups on the selected app.
  • r, y, and x rename, copy, or remove the selected entity.
  • v toggles secret visibility in the detail pane.

Concepts

Groups hold shared environment variables. Apps link one or more groups and can define their own variables that override linked values.

Both apps and groups live in one flat namespace — names must be unique across both kinds.

Names may contain spaces, but not leading or trailing whitespace, commas, or control characters. Commas are reserved for selections.

Selections are comma-separated lists of app and/or group names. They are merged left to right: later entries override earlier ones. Within an app, group values are applied in link order, then the app's own variables are applied last.

The vault is encrypted on disk using age with a post-quantum hybrid recipient. The master password protects a separate encrypted age identity file using age scrypt. Neither file is readable without the password.

Quick Example

The complete flow: create a vault, store secrets, run a command with them injected.

# 1. Create the vault (prompts for a master password)
env-vault init

# 2. Create a shared group with an API key (prompts for the value)
env-vault set --group llm OPENAI_API_KEY

# 3. Create an app that links the group
env-vault set --app chat MODEL gpt-4o
env-vault link chat llm

# 4. Run a command with the chat app's secrets injected
env-vault exec chat -- printenv OPENAI_API_KEY
env-vault exec chat -- printenv MODEL

# 5. Or drop into a subshell with secrets in the environment
env-vault shell chat

Commands

Shared flags apply to every command that reads or writes the vault:

Flag Description
--dir PATH Vault directory (default: ~/.env-vault)
--password-stdin Read the master password from stdin
--password-file PATH Read the master password from a file
--password-fd N Read the master password from an open file descriptor
--unlock-window DURATION Start or extend a short-lived helper process so later commands can reuse the decrypted identity automatically

init

Create a new vault. Prompts for a master password interactively unless a non-interactive source is supplied.

env-vault init [--dir PATH] [--work-factor N] [--password-stdin|--password-file PATH|--password-fd N]

--work-factor N controls the scrypt difficulty (default: 18). Higher values are slower but more resistant to brute force.

env-vault init
env-vault init --dir ~/.config/my-vault
echo "my-password" | env-vault init --password-stdin

info

Show the paths to the vault data file and the encrypted identity file.

env-vault info [--dir PATH]
env-vault info

set

Add or update a variable in an app or group. Without a value argument, prompts for the value with hidden input.

env-vault set [shared flags] [--stdin|--interactive] [--app|--group] NAME [KEY [VALUE]]
  • --app or --group explicitly creates that kind of entity if it does not exist yet. Omit the flag to update an existing entity of either kind.
  • --stdin reads the value from stdin (avoids shell history; requires a different unlock source than --password-stdin).
  • --interactive starts a multi-key edit session in a prompt loop.
  • Passing VALUE as a literal argument works but may leak through shell history and process inspection.
# Prompt for the value (safest)
env-vault set --group llm OPENAI_API_KEY

# Multiple keys in one session
env-vault set --group --interactive llm

# Read value from stdin
echo "sk-abc123" | env-vault set --stdin --group llm OPENAI_API_KEY

# Inline value (shell history risk)
env-vault set --group remotedb DB_HOST db.example.com
env-vault set --app chat MODEL gpt-4o

create

Create an empty app or group. This is useful when you want to establish the vault structure before adding keys or links.

env-vault create [shared flags] app|group NAME
env-vault create group shared
env-vault create app chat

exec

Run a command with secrets injected as environment variables. Use -- to separate vault arguments from the command.

env-vault exec [shared flags] NAME[,NAME...] -- COMMAND [ARGS...]

The selection is merged before injection. The child process inherits the current environment plus all resolved secrets (secrets override any existing env vars with the same name).

env-vault exec chat -- printenv OPENAI_API_KEY
env-vault exec chat,remotedb -- ./start-server
env-vault exec --unlock-window 5m chat -- make test

exec is the preferred way to consume secrets. Unlike export, it never writes plaintext to disk or a terminal.


shell

Start a subshell with secrets injected into its environment.

env-vault shell [shared flags] [--shell PATH] [--allow-nested] NAME[,NAME...] [-- SHELL_ARGS...]
  • --shell PATH overrides the shell binary (default: $SHELL).
  • --allow-nested removes the guard that prevents starting an env-vault shell inside another one.
  • The ENV_VAULT and ENV_VAULT_PROFILE environment variables are set inside the subshell to indicate the active vault and selection.
env-vault shell chat
env-vault shell chat,remotedb
env-vault shell --shell /bin/zsh chat

Add a group to an app so the group's variables are included when the app is resolved.

env-vault link [shared flags] APP GROUP

Groups are merged in the order they are linked. Variables defined directly on the app override all group values.

env-vault link chat llm
env-vault link chat remotedb

Remove a group from an app.

env-vault unlink [shared flags] APP GROUP
env-vault unlink chat remotedb

list

List all apps and groups, or list the resolved keys for a specific selection.

env-vault list [shared flags] [--json] [NAME[,NAME...]]
  • Without a name: shows all apps and groups in the vault.
  • With a name or selection: shows the resolved environment variable keys.
  • --json emits machine-readable output.
env-vault list
env-vault list chat
env-vault list chat,llm --json
env-vault ls

ls is an alias for list.


show

Display detailed information about one entity or a resolved selection.

env-vault show [shared flags] [--resolved] [--json] [app|group] NAME[,NAME...]
  • app or group narrows the lookup when both exist (they cannot, but the qualifier is accepted).
  • --resolved also prints the merged variable values with per-key provenance (which entity each key came from).
  • --json emits machine-readable metadata.
env-vault show group llm
env-vault show app chat
env-vault show --resolved chat
env-vault show --resolved --json chat,llm
env-vault inspect group llm

inspect is an alias for show.


edit

Open an app or group's direct key/value pairs in your $EDITOR.

env-vault edit [shared flags] [--editor PATH] NAME

The editor receives a temporary plaintext file. The file is removed after the editor exits. Treat edit like export from a local-exposure standpoint — plaintext values are on disk while the editor is open.

env-vault edit chat
env-vault edit --editor vim llm

unset

Remove a single key from an app or group.

env-vault unset [shared flags] [--force] NAME KEY

Prompts for confirmation unless --force is given.

env-vault unset chat OPENAI_API_KEY
env-vault unset --force remotedb DB_PASSWORD

remove

Delete an app or group entirely.

env-vault remove [shared flags] [--force] NAME

Prompts for confirmation unless --force is given. Removing a group automatically unlinks it from apps that reference it.

env-vault remove old-app
env-vault remove --force temp-group
env-vault rm old-app

rm is an alias for remove.


rename

Rename an app or group. When a group is renamed, all apps that link it are updated automatically.

env-vault rename [shared flags] OLD_NAME NEW_NAME
env-vault rename remotedb remote-db

copy

Duplicate an app or group under a new name with fresh timestamps.

env-vault copy [shared flags] SOURCE_NAME DEST_NAME
env-vault copy chat chat-staging

export

Write plaintext secrets to stdout or a file.

env-vault export [shared flags] [--format env|export-env|json|dotenv] [--metadata] [--output FILE] [--force-stdout] NAME[,NAME...]
Format Output
env POSIX-safe KEY='value' lines
export-env POSIX-safe export KEY='value' lines for shell sourcing
json flat JSON object
dotenv .env-style KEY="value" lines
  • --metadata wraps the output with entity metadata plus both the direct and resolved env maps (JSON only).
  • --output FILE writes to a file with mode 0600 and prints a warning.
  • --force-stdout is required to print plaintext secrets to an interactive terminal.

exec and shell are safer for most use cases. Use export when you need the data in a file or a format another tool consumes.

env-vault export --format export-env --force-stdout chat
env-vault export --format json --output secrets.json chat
env-vault export --format json --metadata chat,llm
source <(env-vault export --format export-env --force-stdout llm)

unlock

Inspect or stop the short-lived unlock helper started by --unlock-window.

env-vault unlock [--dir PATH] status|clear
  • status shows whether the helper is running and prints its recent activity log.
  • clear stops the helper and removes its socket.

The helper keeps the decrypted age identity in memory only. It communicates over a Unix domain socket (macOS/Linux) or a named pipe (Windows) accessible only to the same user.

env-vault unlock status
env-vault unlock clear

completion

Print a shell completion script.

env-vault completion bash|zsh|fish|powershell
# Bash
eval "$(env-vault completion bash)"

# Zsh
env-vault completion zsh > ~/.zsh/completions/_env-vault

# Fish
env-vault completion fish > ~/.config/fish/completions/env-vault.fish

The generated scripts cover commands and flags. They do not query vault contents for dynamic app and group names.


Non-interactive Unlock

For scripts and CI, supply the password without a prompt:

# From a file
env-vault exec --password-file ~/.vault-pass chat -- ./deploy.sh

# From stdin
echo "my-password" | env-vault list --password-stdin

# Start one unlock window, then reuse it across several commands
env-vault exec --password-file ~/.vault-pass --unlock-window 2m chat -- make build
env-vault exec chat -- make test
env-vault list chat

--password-stdin reads from stdin before the command runs. If you also need set --stdin, use --password-file or --password-fd instead.


Security Notes

  • Secrets prompted interactively use hidden terminal input and are held in locked memory via memguard.
  • set --stdin and set --interactive avoid placing secret values in shell history.
  • Passing VALUE inline on the command line can leak through shell history and ps output.
  • exec and shell are the safe default workflows. export writes plaintext and should be treated as an escape hatch.
  • export refuses to print to an interactive terminal unless --force-stdout is given.
  • export --output FILE creates the file with mode 0600 and prints a warning.
  • edit writes a temporary plaintext file to disk while your editor is open.
  • The --unlock-window helper uses same-user IPC only (Unix socket or Windows named pipe).
  • Atomic writes protect both encrypted files against corruption.
  • Once secrets reach a child process, that process and the OS control further exposure.