Replace .env files with encrypted, OS-native secret storage and a clean CLI developer experience.
deadenv is a cross-platform CLI tool written in Go that eliminates plaintext .env files from your development workflow. Instead of storing credentials as plaintext on disk, deadenv stores them in your operating system's native secret store — Keychain on macOS, Keyring on Linux, or Credential Manager on Windows.
Secrets are retrieved at runtime and injected directly into your subprocesses or shell session. They never touch the filesystem in plaintext.
- Why deadenv?
- Key Features
- Platform Support
- Installation
- Quick Start
- CLI Reference
- Use Cases
- Architecture
- Configuration
- Error Handling
- Testing
- Contributing
The Problem: .env files are security anti-patterns. They store credentials as plaintext on disk, risk accidental commits to git, and can be read by any process or user with filesystem access.
The Solution: deadenv uses your OS's native secret management:
- 🔐 Encrypted Storage: Secrets stored in Keychain, Keyring, or Credential Manager — not on disk
- 🔑 OS-Native Auth: Touch ID, biometrics, or device password gates access
- 📝 Audit Trail: Git-backed history tracking structural changes without storing values
- 📦 Team Sharing: Export encrypted profiles to safely share credentials with teammates
- 🚀 Drop-in Replacement: Works with existing
.envfile formats
✅ Zero Plaintext Storage — Secrets never touch the filesystem
✅ Cross-Platform — macOS, Linux, Windows with native integration
✅ Biometric Support — Touch ID and other OS authentication methods
✅ Profile-Based — Organize secrets by environment or service
✅ Audit Logging — Track who changed what and when (without exposing values)
✅ Secure Export/Import — Share profiles encrypted with AES-256-GCM
✅ Editor Support — Edit secrets interactively with $EDITOR
✅ Multiple Export Formats — Shell scripts, fish syntax, JSON, eval
✅ Minimal Dependencies — Pure Go with OS-native libraries only
| Platform | Keychain Provider | Status | Notes |
|---|---|---|---|
| macOS | Security.framework | ✅ Full | Touch ID & device password support |
| Linux | libsecret / KWallet | ✅ Full | GNOME Keyring, KWallet support |
| Windows | Credential Manager | ✅ Full | Windows Hello integration ready |
# Clone the repository
git clone https://github.com/funinkina/deadenv.git
cd deadenv
# Build the binary
make build
# The binary is available at ./bin/deadenv
./bin/deadenv --help
# Optionally, install to your PATH
sudo mv ./bin/deadenv /usr/local/bin/- Go 1.26+
- macOS: Xcode Command Line Tools (for cgo compilation)
- Linux:
libsecret-1-devandpkg-config - Windows: Standard build tools
deadenv --help# Interactive creation with editor
deadenv profile new myapp
# Create from an existing .env file
deadenv profile new myapp --from=.env.example
# List all profiles
deadenv profile listThe editor will open pre-filled with format instructions. Enter your secrets in KEY=VALUE format (one per line).
# Set with value in argument
deadenv set myapp DATABASE_URL "postgresql://user:pass@localhost/db"
# Set interactively (hidden input)
deadenv set myapp API_KEY# All secrets injected into subprocess environment
deadenv run myapp -- npm start
# Works with any command
deadenv run myapp -- python app.py
deadenv run myapp -- ./my-binary --config=prod# Export the profile encrypted (.deadenv extension added automatically)
deadenv export myapp --out=myapp
# Share myapp.deadenv and the sharing password via separate channels
# Your teammate imports with:
deadenv import myapp.deadenvCreate a new profile.
# Create empty and edit interactively
deadenv profile new staging
# Create from existing .env file
deadenv profile new prod --from=.env.production
# Editor will show the file contents for you to confirm or editProfiles must use lowercase letters, digits, and hyphens (e.g., api-service-dev).
List all available profiles.
deadenv profile lsOutput:
Profiles:
• myapp-dev
• myapp-staging
• payments-service
Show keys in a profile (values masked by default).
# Show with masked values (default)
deadenv profile show myapp
# Output:
# DATABASE_URL [***]
# API_KEY [***]
# DEBUG public-value
# Reveal plaintext values (requires OS authentication)
deadenv profile show myapp --revealDelete a profile and all its keys.
deadenv profile rm old-profile
# Requires confirmation unless --yes is passed
deadenv profile rm old-profile --yesRename a profile (all keys moved to new name).
deadenv profile rename staging staging-oldCopy a profile to a new name (original stays intact).
deadenv profile copy myapp-dev myapp-stagingSet a key in a profile.
# Set with inline value
deadenv set myapp DATABASE_URL "postgresql://localhost/mydb"
# Set interactively (hidden input prompt)
deadenv set myapp API_TOKEN
# Set empty value
deadenv set myapp DEBUG ""Keys must follow POSIX conventions: uppercase letters, digits, underscores; cannot start with a digit.
Retrieve a key's value (masked by default).
# Get masked
deadenv get myapp API_KEY
# Output: [***]
# Get plaintext (requires OS authentication)
deadenv get myapp API_KEY --reveal
# Output: sk_live_51234567890abcdefRemove a key from a profile.
deadenv unset myapp OLD_CONFIG
# Requires confirmation unless --yes is passed
deadenv unset myapp OLD_CONFIG --yesOpen the profile interactively in your editor.
deadenv edit myappThis will:
- Authenticate with OS (to read current values)
- Open
$EDITORwith all current keys pre-populated - Let you add, remove, or modify keys
- Show a diff summary before applying changes
- Write changes back to keychain with audit trail
Changes are granular: each modified key is a separate history entry.
Run a command with profile secrets injected into the environment.
# Basic usage
deadenv run myapp -- npm start
# With complex arguments
deadenv run myapp -- python -m flask run --host=0.0.0.0
# Chaining with pipes (entire expression runs with injected env)
deadenv run myapp -- bash -c "npm build && npm start"
# Database migrations with environment-specific connection string
deadenv run myapp-prod -- psql -c "CREATE TABLE ..."Important: Use the -- separator to prevent flag parsing conflicts.
The subprocess inherits all current environment variables plus the profile's secrets (profile values override existing vars, consistent with .env tools).
Exit code is propagated: if the command exits with code 42, so does deadenv.
Export secrets for shell evaluation.
# Generate shell export commands (bash/zsh)
deadenv export myapp
# Output:
# export DATABASE_URL="postgresql://localhost/mydb"
# export API_KEY="sk_live_..."
# Eval into current shell (bash/zsh)
eval $(deadenv export myapp)
# Fish shell syntax
deadenv export myapp --format=fish | source
# JSON for machine consumption
deadenv export myapp --format=json
# Output: [{"key":"DATABASE_URL","value":"..."}...]
# Write to a shell script file
deadenv export myapp --out=./env.sh
source ./env.shValues are properly shell-escaped. The export is read-only; changes must be made with deadenv set or deadenv edit.
Create an encrypted .deadenv export file (portable format for sharing). The .deadenv extension is added automatically if not provided.
# Export as encrypted file (extension added automatically)
deadenv export myapp --out=myapp
# Both of these work:
deadenv export myapp --out=myapp
deadenv export myapp --out=myapp.deadenv
# You will be prompted for a sharing password (separate from OS auth)
# Enter sharing password: ****
# Confirm password: ****
# ✓ Profile exported to myapp.deadenvThe export file is:
- Self-contained: includes all KDF parameters for decryption
- Versioned: supports future format migrations
- AES-256-GCM encrypted: uses a password-derived key (Argon2id)
- Secure: even with filesystem access, passwords are required to decrypt
Import an encrypted .deadenv file.
# Import with original profile name
deadenv import myapp.deadenv
# Enter sharing password: ****
# Import 8 keys from profile "myapp"? (y/n): y
# ✓ Profile "myapp" imported successfully
# Import as different profile name
deadenv import myapp.deadenv --as=myapp-importedOn import:
- You're prompted for the sharing password
- A summary of keys to import is shown
- You confirm before any keychain writes
- The import is recorded in the audit history
View audit history of changes (structural changes only, no values).
# View full history for profile
deadenv history myapp
# Output:
# Profile: myapp
# ─────────────────────────────────────────────────
# 2025-04-21 14:30 set API_KEY (hash: a3f1b2...)
# 2025-04-21 14:25 modified DATABASE_URL (hash: 7c2d9e...)
# 2025-04-21 14:20 set DEBUG (hash: 5e41d3...)
# Filter to a specific key
deadenv history myapp --key=API_KEY
# Output:
# Profile: myapp (KEY: API_KEY)
# ────────────────────────────────
# 2025-04-21 14:30 set (hash: a3f1b2...)
# 2025-04-19 10:15 modified (hash: 9c8b4f...)Each entry shows:
- Timestamp: when the change was made
- Operation:
set,modified,unset,delete - Key name: which secret was affected
- Hash: SHA-256(salt + value) — proves the value changed without revealing it
This history is stored in a local git repository (~/.config/deadenv/history/) for durability and traceability.
Print a shell hook snippet for convenient profile switching.
# Generate for your shell
deadenv init --shell=zsh
# Output: (copy and paste into ~/.zshrc or ~/.bashrc)
# deadenv() {
# if [[ "$1" == "use" ]]; then
# export -p $(deadenv export "$2")
# else
# command deadenv "$@"
# fi
# }
# After pasting and sourcing, you can use:
deadenv use myapp
# Now all vars are in your current shell (no subprocess)
# To run a command with profile active:
deadenv run myapp -- npm startProblem: You have .env.local files with credentials that could be accidentally committed.
Solution:
# One-time setup
deadenv profile new myapp --from=.env.local
rm .env.local .env.local.*.backup # Remove plaintext copies
# Daily usage: run your app with secrets injected
deadenv run myapp -- npm start
# Or source into your shell for a persistent session
eval $(deadenv export myapp)
npm startBenefit: No plaintext credentials on disk. No risk of accidental commits.
Problem: A new team member joins and needs credentials. Sharing them via Slack or email is insecure.
Solution:
Team Lead:
# Export the profile (.deadenv extension added automatically)
deadenv export myapp --out=myapp
# Share the file via any channel (it's encrypted)
# Share the password via a separate, secure channel (Signal, 1Password, etc.)New Team Member:
# Receives: myapp.deadenv file + password
deadenv import myapp.deadenv
# Enter sharing password: [provided via secure channel]
# ✓ All 12 keys importedBenefit: Credentials never sent in plaintext. Easy one-time setup.
Problem: CI systems need credentials but shouldn't store them in plaintext.
Solution:
# In your CI pipeline:
deadenv run myapp-ci -- npm run build
# Secrets are injected into the build subprocess
# Exit code is propagated for CI failure detectionAlternative: Export to a file at deployment time (outside the repository):
deadenv export myapp-prod --format=json | \
jq -r '.[] | "\(.key)=\(.value)"' > deploy-env
# Pass deploy-env to your container/lambda/etc.Problem: You work on dev, staging, and production configs with different secrets.
Solution:
# Create separate profiles
deadenv profile new myapp-dev --from=.env.dev.example
deadenv profile new myapp-staging --from=.env.staging.example
deadenv profile new myapp-prod --from=.env.prod.example
# Switch between environments
deadenv run myapp-dev -- npm start # Dev database
deadenv run myapp-staging -- npm start # Staging database
deadenv run myapp-prod -- npm start # Production database (use with caution!)
# Edit a specific environment's secrets
deadenv edit myapp-stagingBenefit: Clear separation of environments. Audit trail shows which was changed when.
Problem: An API key is compromised and must be rotated.
Solution:
# Check current value (masked by default)
deadenv profile show myapp
# Update the compromised key
deadenv set myapp API_KEY "sk_live_new_secret_..."
# Audit history records both old and new (without values)
deadenv history myapp --key=API_KEY
# 2025-04-21 15:45 modified API_KEY (hash: new_hash...)
# 2025-04-21 10:00 set API_KEY (hash: old_hash...)Benefit: Incident response is traceable. Values never logged or exposed.
deadenv/
├── main.go # Entry point, error routing
├── cmd/ # CLI command definitions (thin wrappers)
│ ├── profile.go # Profile subcommands
│ ├── set.go, get.go, unset.go # Key management
│ ├── edit.go # Interactive profile editing
│ ├── run.go # Subprocess injection
│ ├── export.go, import.go # Export/import logic
│ ├── history.go # Audit log display
│ └── init.go # Shell hook generation
├── internal/
│ ├── keychain/ # OS keychain abstraction
│ │ ├── keychain.go # Interface + types
│ │ ├── keychain_darwin.go # macOS implementation
│ │ ├── keychain_linux.go # Linux implementation
│ │ ├── keychain_windows.go # Windows implementation
│ │ ├── fake.go # Test double
│ │ └── service.go # Helper functions
│ ├── parser/ # .env format parser (pure function)
│ │ ├── parser.go
│ │ └── fuzz_test.go
│ ├── crypto/ # AES-256-GCM export encryption
│ │ ├── crypto.go
│ │ ├── types.go
│ │ └── errors.go
│ ├── history/ # Git-backed audit log
│ │ ├── history.go # Interface
│ │ ├── git_recorder.go # Git implementation
│ │ ├── noop.go # No-op (for --no-history)
│ │ ├── fake_history.go # Test double
│ │ └── hash.go # Value hashing
│ ├── profile/ # Profile orchestration
│ │ ├── profile.go # CRUD operations
│ │ ├── edit.go # Editor flow + diff logic
│ │ ├── populate.go # Profile population from files
│ │ ├── serialize.go # Format for editor display
│ │ ├── diff.go # Change detection
│ │ └── errors.go # Sentinel errors
│ ├── tui/ # Terminal UI helpers
│ │ ├── tui.go # Prompts, masked input, tables
│ │ └── colors.go # ANSI styling
│ └── envPair/ # Key-value pair type
│ └── envPair.go
├── testutil/ # Shared test utilities
│ └── testutil.go
├── go.mod, go.sum # Dependencies
├── Makefile # Build targets
└── README.md
Interfaces Over Implementations
keychain.Store— abstracts platform-specific keychain accesshistory.Recorder— abstracts git history recording- Both are injected, making code testable with fakes
Pure Functions
- Parser takes a string, returns pairs or error — no I/O
- Crypto functions are deterministic — fully testable
- Parser fuzz tests validate robustness
Thin CLI Layer
cmd/packages are wrappers aroundinternal/logic- No business logic in CLI handlers
- All errors flow through
main.gofor consistent exit codes
Lenient Parsing
- Unmatched quotes are treated as literals, not errors
- This matches
.envtool behavior and improves compatibility
| Variable | Purpose | Default |
|---|---|---|
DEADENV_CONFIG |
Override config directory | ~/.config/deadenv |
EDITOR |
Editor for deadenv edit |
$VISUAL or vi |
VISUAL |
Alternative to EDITOR |
(not set) |
~/.config/deadenv/ contains:
~/.config/deadenv/
├── history/ # Git repository for audit log
│ └── <profile>.json # History entries for each profile
└── .salt # Random salt for value hashing (created once)
All deadenv commands support:
--config <dir> # Override config directory
--no-history # Skip git commit for this operation
--quiet # Suppress informational output
--yes # Skip confirmations (dangerous operations)Examples:
# Use alternate config directory
deadenv --config=/data/deadenv profile list
# Skip history tracking for this operation
deadenv --no-history set myapp TEMP_KEY "value"
# Delete without prompt (be careful!)
deadenv profile rm myapp --yes| Code | Meaning | Example |
|---|---|---|
0 |
Success | Any successful command |
1 |
General error | Profile not found, bad args, validation error |
2 |
Auth failure | OS keychain denied access (user rejected Touch ID) |
3 |
Decrypt failure | Wrong import password or corrupted file |
4 |
Parse error | Malformed env content, invalid key name |
127 |
Command not found | Subprocess in deadenv run not found |
N |
Propagated exit code | Exit code from deadenv run -- <command> |
All errors:
- Go to
stderr(normal output tostdout) - Include helpful context
- Never expose secret values
- Suggest corrective action
Example:
$ deadenv get myapp NONEXISTENT
Error: key "NONEXISTENT" not found in profile "myapp"
Set it with: deadenv set myapp NONEXISTENTmake testThis runs unit tests with race detection.
go test ./internal/parser -vIncludes table-driven tests for all .env format variants.
make fuzzRuns the parser against randomized input for 60 seconds to find edge cases.
make integrationTests against real OS keychain and git. Requires:
- Keychain/Keyring/Credential Manager to be functional
- Git to be installed
- No prompt for authentication (in CI, use
--no-history)
go test -cover ./...git clone https://github.com/funinkina/deadenv.git
cd deadenv
make build
./bin/deadenv --help# macOS (arm64/Apple Silicon)
GOOS=darwin GOARCH=arm64 go build -o bin/deadenv-darwin-arm64
# Linux (x86_64)
GOOS=linux GOARCH=amd64 go build -o bin/deadenv-linux-amd64
# Windows (x86_64)
GOOS=windows GOARCH=amd64 go build -o bin/deadenv-windows-amd64.exemake lintRequires golangci-lint. Install with:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh- Create
cmd/mycommand.gowith aNewMyCommand()function - Register in
cmd/root.go - Add business logic to
internal/packages - Write tests in
internal/<package>/*_test.go - Update this README with examples
Problem: deadenv history displays a warning or history is disabled.
Solution: Install Git. Once installed, history will work automatically.
# macOS
brew install git
# Ubuntu/Debian
sudo apt-get install git
# Fedora
sudo dnf install gitProblem: You get "profile myapp not found" when trying to access it.
Solution: List available profiles and create one if needed.
deadenv profile list
deadenv profile new myappProblem: Import fails with "decryption failed — wrong password or file is corrupted."
Solution:
- Verify you're using the correct sharing password
- Confirm the
.deadenvfile is intact (not truncated, not moved) - Try re-exporting from the source machine
Problem: OS Keychain access is denied the first time you run deadenv.
Solution: This is normal. Grant permission by:
- Clicking "Allow" in the OS prompt, or
- Using
deadenv setwhich will prompt you through Keychain setup
Problem: deadenv reports libsecret or KWallet is not available.
Solution: Install the keyring service:
# GNOME Keyring (Ubuntu/Debian/Fedora)
sudo apt-get install gnome-keyring
# Or KWallet (KDE)
sudo apt-get install kwalletmanagerThen restart your session or manually start the service:
dbus-daemon --session &
/usr/bin/gnome-keyring-daemon --start &Problem: Credentials don't appear in Credential Manager GUI.
Solution: This is expected. deadenv stores items with a special prefix (deadenv/<profile>). They are accessible only through deadenv for security. To verify they're stored:
# List keys in a profile (masked)
deadenv profile show myapp
# Reveal values (requires Windows Hello or password prompt)
deadenv profile show myapp --reveal✅ Secrets are stored in OS-native encrypted storage
✅ OS-native authentication gates all read access
✅ Export files are encrypted with AES-256-GCM + Argon2id
✅ History contains no plaintext values (only hashes)
✅ Temp files used during editing are created with 0600 permissions
- Never commit
.envfiles or.deadenvexports to git - Share
.deadenvfiles and passwords via separate channels (file in email, password via Signal) - Rotate secrets regularly, especially if machines are shared
- Review audit history with
deadenv history <profile>to catch unauthorized changes - Enable OS lock timeout on your machine so keychain locks when you step away
- Team sync: Real-time secret sharing with team members (post-v1)
- IDE plugins: VS Code, JetBrains, etc. (post-v1)
- GitHub Actions integration: Automatic secret injection in CI workflows (post-v1)
- Secret rotation: Automatic or manual rotation with versioning (post-v2)
- Access policies: Fine-grained permissions for shared machines (post-v2)
- Backup/restore: Encrypted backups of all profiles (post-v2)
Contributions are welcome! To contribute:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Write tests for your changes (run
make test) - Lint your code:
make lint - Commit with clear messages
- Push to your fork
- Open a pull request with a description of your changes
# Install Go 1.26+
# Install golangci-lint for linting
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh
# Install dependencies
go mod download
# Build and test
make build test lintWhen reporting bugs, include:
- OS and version (macOS 14.1, Ubuntu 22.04, Windows 11, etc.)
- Go version:
go version - Error message and context
- Steps to reproduce
- Whether you're using a real keychain or running tests
Avoid sharing actual credentials in issue reports.
MIT License — see LICENSE file for details.
- Documentation: See this README and
deadenv-prd.mdfor detailed specs - Issues: Report bugs on GitHub
- Security issues: Please disclose privately to the maintainers
- Built with urfave/cli for CLI framework
- Uses platform-specific keychain libraries for secure storage
- Inspired by best practices in secret management tools like Vault and 1Password
- PRD (Product Requirements Document)
- Architecture Documentation (forthcoming)
- API Reference (forthcoming)
Made with ❤️ for developers who value security.