A fast, Telescope-style terminal UI
(and full CLI) for managing your local development secrets — organized by
project and environment instead of scattered .env files.
devsecrets · written in Rust · TUI + scriptable CLI
🌐 peterkracik.github.io/dev-secrets · Releases
If you work on several projects, each with a few environments (dev,
staging, prod, a teammate's setup, …), you usually end up with a mess of
.env files copied between folders and slowly drifting out of sync.
dev-secrets keeps all of those values in one place, grouped logically:
Project e.g. "api"
└── Environment e.g. "dev", "staging", "prod" (an "instance")
└── Secrets KEY = VALUE pairs
When you sit down to work, you export the right set on demand:
devsecrets export .env # writes this folder's assigned environment to ./.envEverything you can do in the interactive TUI you can also do from the CLI, so it fits into scripts and Makefiles just as well as interactive use.
Warning
This is not a secrets manager for production credentials.
The store is plain, human-readable JSON on your disk and is not
encrypted. It is designed for local, non-sensitive, test values only
(think localhost URLs, dummy API keys, dev ports). Do not put real
production secrets in here.
- 🗂️ Organized by project → environment → secret, not by folder.
- 🔭 Telescope-style TUI — centered floating window, fuzzy search, live preview.
- ⌨️ Full CLI parity — every action is scriptable.
- 📥 Import existing
.envfiles (merge or replace). - 📤 Export any environment to a file or stdout.
- 🔗 References: write
${project.env.KEY}to reuse a value instead of duplicating it. Resolved (recursively, with cycle detection) on export. - 📌 Folder assignments —
devsecrets setupbinds a folder to a project/env, sodevsecrets export(and the TUI) "just know" what to use. - 📋 Clipboard copy of a secret value or a whole environment.
- 📝 Plain JSON store you can read, diff, and back up yourself.
dev-secrets is a single self-contained binary called devsecrets.
brew install peterkracik/tap/devsecrets…which is shorthand for:
brew tap peterkracik/tap
brew install devsecretsUpgrade later with brew upgrade devsecrets.
Grab the archive for your platform from the Releases page (built automatically by CI), or pull the latest release from the command line:
# Linux (x86_64)
curl -L https://github.com/peterkracik/dev-secrets/releases/latest/download/devsecrets-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo install devsecrets-x86_64-unknown-linux-gnu/devsecrets /usr/local/bin/
# macOS (Apple Silicon)
curl -L https://github.com/peterkracik/dev-secrets/releases/latest/download/devsecrets-aarch64-apple-darwin.tar.gz | tar xz
sudo install devsecrets-aarch64-apple-darwin/devsecrets /usr/local/bin/Available targets: x86_64-unknown-linux-gnu, aarch64-apple-darwin,
x86_64-apple-darwin, and x86_64-pc-windows-msvc (.zip).
Requires Rust 1.74+ (the only build dependency — no system libraries needed).
cargo install --git https://github.com/peterkracik/dev-secrets.git
# or, from a clone:
git clone https://github.com/peterkracik/dev-secrets.git && cd dev-secrets
cargo install --path .This places devsecrets in ~/.cargo/bin (ensure it's on your PATH).
devsecrets --version
devsecrets --helpdevsecrets is a lot to type for a tool you reach for constantly. We recommend
aliasing it to ds. Add this to your shell's startup file (~/.bashrc,
~/.zshrc, …):
alias ds=devsecretsReload your shell (or source the file) and everything in this README works
with ds:
ds setup
ds secret list # this folder's secrets
ds env list # this folder's environments
ds export .envThe rest of the docs spell out devsecrets in full, but ds is interchangeable
once the alias is set.
# 1. In your project folder, run setup. It walks you through choosing
# (or creating) a project and environment, and remembers this folder.
cd ~/code/api
devsecrets setup
# 2. Add some secrets
devsecrets secret set -p api -e dev DB_HOST localhost
devsecrets secret set -p api -e dev DB_PORT 5432
# 3. Export them when you need them — no flags, this folder is assigned
devsecrets export .env…or just run the TUI and do all of the above interactively:
devsecretsEverything lives under ~/.config/devsecrets/:
| File | Purpose |
|---|---|
settings.toml |
Points at where the secrets store file lives. |
meta.json |
Folder → (project, environment) assignments (see below). |
store.json |
The secrets themselves (default location). |
The config directory is platform-specific:
| OS | Default location |
|---|---|
| Linux / macOS | ~/.config/devsecrets/ |
| Windows | %APPDATA%\devsecrets\ |
You can override the base directory with the standard XDG_CONFIG_HOME
environment variable (Unix).
setup configures the current folder: it asks (the first time) which
project and environment this folder belongs to, and remembers the
folder → (project, env) assignment in meta.json. From then on, whenever you
work in that folder, dev-secrets knows what to use — a bare devsecrets export
or launching the TUI jumps straight to the right project/environment.
cd ~/code/api
devsecrets setup # wizard: pick/create project, then environment
# ...later, from the same folder:
devsecrets export .env # no flags needed — uses this folder's project/envThe wizard:
- Project — pick an existing project by number, or type a name to create one.
- Environment — pick or create an environment within that project.
setup is skipped (no-op wizard) when input isn't an interactive terminal, so
it stays scriptable. You can also assign a different folder explicitly:
devsecrets setup /path/to/folder. Inside the TUI, press f to assign the
current folder to the selected project/environment.
devsecrets versionprints the version plus the config locations and current folder assignments:
devsecrets 0.1.0
Config dir: ~/.config/devsecrets
Settings file: ~/.config/devsecrets/settings.toml
Meta file: ~/.config/devsecrets/meta.json
Store file: ~/.config/devsecrets/store.json
Folder assignments:
/home/you/code/api → api/dev
The store file location lives in settings.toml as store_path. To keep it
somewhere else (e.g. a synced folder shared across your machines), move the
file and point store_path at the new location:
# ~/.config/devsecrets/settings.toml
store_path = "/home/you/Dropbox/devsecrets/store.json"(Remember: unencrypted — local test values only.)
Run devsecrets with no arguments. The interface is
Telescope-style: a
centered, bounded floating window (it never fills the whole terminal) with a
fuzzy-filter prompt, a results list with match highlighting, and a live
preview pane.
Projects and environments share one tree — projects with their environments nested underneath — which makes fuzzy navigation fast:
▾ api (2 env)
– dev ★ 3 keys
– prod 3 keys
▸ web (1 env) ← collapsed
Press / to fuzzy-filter; it matches both project and environment names.
A match in an environment keeps its project header visible; a match on a
project keeps all of its environments. Projects can be collapsed to hide their
environments. Opening an environment drills into its Secrets screen.
| Key | Action |
|---|---|
/ |
Fuzzy-filter (matches projects and environments) |
↑/k ↓/j |
Move selection (Ctrl-n/Ctrl-p while searching) |
→/l |
Expand a project / open an environment → Secrets |
←/h |
Collapse a project / jump to the parent project |
Enter |
Toggle a project (collapse), or open an environment |
Esc |
Back out of the Secrets screen |
p |
New project |
n |
New environment (under the selected project) / secret |
e |
Edit a secret, or the selected env inline |
a |
Edit the whole environment inline (multi-line) |
E |
Edit the whole environment in $EDITOR (as .env) |
g |
Jump to the secret a $ref: value points at |
c |
Copy: secret value (on Secrets) / whole env (else) |
C |
Copy the whole environment as a .env document |
d |
Delete the selected project / env / secret (confirm) |
y |
Duplicate the selected environment |
i |
Import a .env file into the selected environment |
x |
Export the selected env (path + format in one dialog) |
D |
Set the selected environment as the project default |
f |
Assign the current folder to this project/env |
s |
Toggle showing / hiding secret values |
? |
Help overlay |
q / Ctrl-C |
Quit |
New secrets are entered through a two-field form (Key and Value as
separate boxes, Tab to switch) so a value can be pasted on its own. Values
are masked by default — press s to reveal them. The preview pane shows the
selected item's details, including a secret's resolved value when it contains
${...} references.
Press c to copy the selected secret's resolved value, or C (also c when
not on a single secret) to copy the whole environment as a resolved .env
document. dev-secrets uses the system clipboard tool when available
(pbcopy, wl-copy, xclip/xsel, clip) and otherwise falls back to the
terminal's OSC 52 sequence, which also works over SSH and tmux.
You can edit the entire environment as one .env document — two ways:
- Inline (
a, orewhen not on a single secret): a built-in multi-line editor opens inside the TUI. Type freely;Ctrl-Ssaves and applies,Esccancels. No external tools required. - External (
E): opens the environment in your editor ($VISUAL, then$EDITOR, falling back tovi/notepad). Save and quit to apply; quit without saving to cancel.
Either way, add/change/reorder/remove lines freely; on save the document is
parsed back and replaces the environment's contents. References like
${project.env.KEY} are preserved verbatim and resolved on export.
Press i, give a path, then choose how to apply it:
- merge — add new keys and confirm each changed key individually
(
yoverwrite ·nkeep ·aoverwrite all ·qstop); - overwrite — add new keys and replace every changed key without asking;
- replace — clear the environment first, then load only the file.
The CLI mirrors this: devsecrets import file.env prompts per changed key in a
terminal, or use --overwrite / --replace. With no terminal (scripts), the
default leaves existing keys untouched unless you pass --overwrite.
Everything below also works interactively in the TUI.
devsecrets setup [<folder>] # assign a folder to a project/env (wizard)
devsecrets version # version + config locations + folder assignmentsdevsecrets project create <name>
devsecrets project list
devsecrets project delete <name>devsecrets env create -p <project> <name>
devsecrets env list [-p <project>]
devsecrets env set-default -p <project> <name>
devsecrets env delete -p <project> <name>env list drops the -p flag once the folder is assigned (devsecrets setup): a bare devsecrets env list shows the environments of this folder's
project.
devsecrets secret set -p <project> -e <env> <KEY> <VALUE> [--type text|number|json]
devsecrets secret get -p <project> -e <env> <KEY> [--raw]
devsecrets secret list [-p <project>] [-e <env>] [--mask]
devsecrets secret delete -p <project> -e <env> <KEY>get resolves references by default (use --raw for the literal value).
list shows values by default; pass --mask to obscure them.
Like export and import, secret list drops the -p/-e flags once the
folder is assigned (devsecrets setup). From inside an assigned folder, a bare
devsecrets secret list shows that folder's secrets (secrets is also accepted
as an alias for secret):
cd ~/code/api
devsecrets secret list # no flags — uses this folder's project/env
devsecrets secrets list # same thingValue types. Each secret has a type — text (default), number, or
json — which is validated on set. --type number rejects non-numbers and
--type json rejects invalid JSON. In the TUI, the new-secret form has a Type
field (←/→ to change), and a json value is pretty-printed in the preview
when revealed (s).
# Import a .env file. Like export, -p/-e are optional once the folder is
# assigned (devsecrets setup). New keys are added automatically; for each
# CHANGED key you're asked whether to overwrite (interactive menu).
devsecrets import existing.env
devsecrets import existing.env -p api -e dev
# Skip the per-key prompts:
devsecrets import existing.env --overwrite # overwrite all changed keys
devsecrets import existing.env --replace # clear the env, then load the file
# Export to a file
devsecrets export .env -p api -e dev
# Export the project's default environment to stdout
devsecrets export -p api
# Export using this folder's assigned project + env (after `devsecrets setup`)
devsecrets export
# Choose a format explicitly, or let the file extension decide
devsecrets export --format json -p api -e dev # to stdout as JSON
devsecrets export config.toml -p api -e dev # inferred TOML
devsecrets export -p api --format shell # export KEY=VALUE lines
# Export raw values (do not resolve ${...} references)
devsecrets export .env -p api --rawFormats (--format, or auto-detected from the file extension):
| Format | Looks like | Extensions |
|---|---|---|
env |
KEY=VALUE (default) |
.env |
shell |
export KEY=VALUE |
.sh .bash .zsh |
json |
{ "KEY": "VALUE" } |
.json |
toml |
KEY = "VALUE" |
.toml |
shell is handy for eval "$(devsecrets export --format shell)".
devsecrets duplicate -p api dev stagingdevsecrets listA value may reference another secret so a shared value lives in exactly one place. There are three forms, depending on how much context you need to repeat — they resolve relative to where the value lives:
| Form | Resolves to |
|---|---|
${SECRET} |
same project, same environment |
${env.SECRET} |
same project, another environment |
${project.env.SECRET} |
anywhere in the store |
devsecrets secret set -p api -e dev DB_HOST localhost
devsecrets secret set -p api -e dev URL 'http://${DB_HOST}:5432' # same env
devsecrets secret set -p api -e prod URL 'https://${dev.DB_HOST}/api' # other env
devsecrets secret set -p api -e dev DSN '${infra.prod.DATABASE_URL}' # cross-project
devsecrets secret get -p api -e dev URL
# → http://localhost:5432- References are resolved at export time (and by
secret get). - They can be nested (a referenced value may contain more references), and each level resolves relative to its own project/env.
- Cycles and missing targets are detected and reported as errors.
- Use
--rawonexport/getto keep references literal.
Autocomplete: in the TUI, while editing a secret value (or the value field
of a new secret), type ${ to pop up a fuzzy-filterable list of available
secrets. Use ↑/↓/Tab to choose and Enter to insert the right form
(${SECRET} / ${env.SECRET} / ${project.env.SECRET} depending on where it
lives); Esc dismisses the popup.
Display & jump: while values are hidden, references show as
$ref:project.env.key (e.g. AUTH = $ref:shared.TOKEN, with a coloured
$ref: prefix) and ordinary values are masked. Press s to reveal: ordinary
values appear and references resolve to their actual value (in the secrets
list and the environment preview). Press g on a referencing secret to jump
straight to the secret it points at.
Because devsecrets export writes a standard .env (and, after
devsecrets setup, needs no arguments inside an assigned folder), it drops
straight into direnv.
Add an .envrc to your project folder:
# .envrc — regenerate .env from dev-secrets and load it
devsecrets export .env
dotenv .envThen:
echo '.env' >> .gitignore # don't commit the generated file
direnv allowNow every time you cd into the project, direnv refreshes .env from the
folder's assigned project/environment and loads the variables into your shell;
they're unloaded when you leave.
Prefer not to write a file? Load straight from stdout instead:
# .envrc
set -a # auto-export everything that follows
eval "$(devsecrets export)" # this folder's assigned project/env
set +aThe same devsecrets export .env line works in a Makefile, a container
entrypoint, or a CI step.
It's just JSON — readable, diffable, and easy to back up. store.json:
{
"projects": {
"api": {
"default_env": "dev",
"environments": {
"dev": {
"values": {
"DB_HOST": "localhost",
"DB_PORT": "5432"
}
}
}
}
}
}…and meta.json records folder assignments:
{
"assignments": {
"/home/you/code/api": { "project": "api", "env": "dev" }
}
}Is it safe to commit my store to Git? Only if it truly contains non-sensitive test values, and ideally in a private repo. The file is not encrypted, so treat it like any other plaintext config.
Can I edit store.json by hand?
Yes. Keep it valid JSON; key order is preserved on save so diffs stay clean.
Does it support comments / export FOO=bar in .env files?
On import, yes — blank lines, # comments, an optional export prefix, and
single/double quoted values are all understood.
Can a value be a JSON object, contain =, spaces, or newlines?
Yes. Values are arbitrary strings and are stored verbatim. On .env
export/editing they are double-quoted and escaped only when needed (e.g. a
{"a": 1} JSON value, a multi-line cert, or a Windows path), so the
round-trip is lossless.
What happens if I run a command before setup?
Everything works — the store is created automatically on first run. setup
only records a folder → project/env assignment so you can drop the
--project/--env flags in that folder. Use explicit flags anywhere else, and
devsecrets version to see where the store lives.
cargo build # debug build
cargo test # unit + integration tests (tests/cli.rs drives the binary)
cargo clippy # lints
cargo fmt # formatCI (GitHub Actions) runs fmt, clippy, and the unit + integration tests on every
push/PR (.github/workflows/ci.yml). On pushes to main and on v* tags,
.github/workflows/release.yml cross-builds binaries for Linux, macOS
(Intel + Apple Silicon), and Windows, uploads them as build artifacts, and — for
tags — attaches them to a GitHub Release. The Homebrew tap formula is
Formula/devsecrets.rb.
Contributions and issues are welcome.
MIT
