envkit
Portable env/dotfile manager with keychain-backed secrets
git@gitlab.com:aice-lab/envkit.git
Latest release
v0.2.1 ·
README
envkit
Your dotfiles in git, your secrets in the keychain — on macOS and Linux.
envkit backs up your environment files (shell configs, editor configs, scripts) to a git repository and restores them on any machine — while keeping secrets out of git and out of plaintext on disk. Secrets live in your OS keychain (macOS Keychain / Linux pass) and are looked up at runtime.
brew tap aice-lab/tap https://gitlab.com/aice-lab/homebrew-tap.git
brew install envkit
Why another dotfile manager?
Most dotfile tools symlink your ~ files into a repo, which couples your machine to that repo — delete the repo and your shell breaks. And they have no good answer for secrets, so people either commit tokens (bad) or keep a pile of manual steps (worse).
envkit takes a different stance:
- 🔗 Decoupled, not symlinked. Your
~/.zshrcstays a real file. The repo is a backup you push to and pull from. If the repo vanishes, your machine keeps working. - 🔑 Secrets never touch git. Declare which values are secret; envkit moves them into the OS keychain and leaves a placeholder in the repo. Your shell fetches them at startup. No tokens in your dotfiles, ever.
- 🖥️ One store, many machines. Per-OS layers mean the same store gives your Mac its
.zshrcand your Linux box its.bashrc, while sharing.gitconfigbetween them. - ↔️ No magic sync. Two explicit directions —
backup(machine → repo) andload(repo → machine) — so envkit never guesses which side wins a conflict.
Install
Homebrew (macOS + Linux)
brew tap aice-lab/tap https://gitlab.com/aice-lab/homebrew-tap.git
brew install envkit
On Linux this also pulls in pass (the secret backend).
From source / direct download
go install gitlab.com/aice-lab/envkit/cmd/envkit@latest
Prebuilt darwin/{amd64,arm64} and linux/{amd64,arm64} binaries are attached to every release.
Tutorial
We’ll go from zero to “my dotfiles and a secret are safely backed up,” then restore them on a second machine.
1. Create a store repo
Your store is just a private git repo that will hold your dotfiles. Make one (on GitLab/GitHub/anywhere) and clone it:
git clone git@gitlab.com:you/dotfiles.git ~/dotfiles
A plain local directory works too (envkit will warn that it’s disk-only with no off-machine copy) — but a git repo is strongly recommended.
2. Point envkit at it
envkit init --store ~/dotfiles
This writes ~/.config/envkit/config.toml and creates an empty envkit.toml manifest in the store.
init also scans ~ for common dotfiles (.zshrc, .gitconfig, .vimrc, the
bash/zsh rc family, …) and tracks the ones it finds, printing a summary. Files
that look like they hold secrets are skipped with guidance to re-add them via
--secret. Pass --no-scan to skip this (recommended on a restore machine —
there you want init --no-scan then load, so the second machine’s stock
dotfiles don’t get added to the shared store).
doctor confirms things look right:
envkit doctor
# ok config + store loadable
# ok store backup: git-backed with remote
3. Track your first files
add copies a file into the store and starts tracking it. envkit auto-detects how to handle well-known files:
envkit add .gitconfig # shared, plain file
envkit add .vimrc # shared, plain file
envkit add .zshrc # recognized shell rc → tracked as a shell file, OS-specific
You can add several at once, or import a list:
envkit add .gitconfig .vimrc .zshrc # multiple in one go
envkit add --from ~/my-dotfiles.txt # newline-delimited list (# comments ok)
.zshrc,.bashrc,.p10k.zsh, etc. are recognized shell configs → routed to your current OS’s layer and markedkind = shell.- Everything else defaults to shared.
- Override with
--os(force current-OS layer),--shared, or--kind shell|dotenv|plain.
Your manifest (~/dotfiles/envkit.toml) now looks like:
[[file]]
path = ".gitconfig"
[[file]]
path = ".vimrc"
[[file]]
path = ".zshrc"
kind = "shell"
4. Add a secret
Say your .zshrc exports a token. Tell envkit it’s a secret:
envkit rm .zshrc # re-add with the secret declared
envkit add .zshrc --secret GITHUB_TOKEN
Now on backup, envkit will pull the value out of .zshrc, store it in your keychain, and leave a placeholder in the repo. At shell startup your .zshrc sources a generated file that fetches it back:
# ~/.config/envkit/secrets.zsh (generated; never committed)
export GITHUB_TOKEN="$(envkit secret get GITHUB_TOKEN)"
You can also manage keychain secrets directly:
envkit secret set OPENAI_API_KEY # prompts for the value
envkit secret get OPENAI_API_KEY
envkit secret list
5. Back up
Preview, then back up, then push:
envkit backup --dry-run # show what would be written to the store
envkit backup # ~ → store (secrets → keychain, placeholders → repo)
envkit status # everything should read "in-sync"
Commit and push the store with your normal git flow, or let envkit do it:
envkit backup --force-push -m "back up dotfiles"
That’s it — your dotfiles are in git and your token is in the keychain, never on disk in plaintext.
6. Restore on another machine
Install envkit, clone the same store, and load:
brew install envkit # (after tapping)
git clone git@gitlab.com:you/dotfiles.git ~/dotfiles
envkit init --store ~/dotfiles
envkit load # repo → ~
load writes your dotfiles, regenerates the secrets lookup file, and prompts for any secret it doesn’t have yet. It is safe by default: if a file already exists locally and differs, it’s skipped with a warning (use --force to overwrite). Anything it does overwrite is first copied to ~/.envkit-backups/<timestamp>/.
7. Mac and Linux from one store
Your Mac uses zsh (.zshrc); your Linux box uses bash (.bashrc). They’re different files, so envkit keeps them in per-OS layers while sharing the rest:
~/dotfiles/
├── envkit.toml # the manifest (shared across machines)
├── files/ # shared: loaded on every OS
│ ├── .gitconfig
│ └── .vimrc
├── os/darwin/ # macOS only
│ └── .zshrc
└── os/linux/ # Linux only
└── .bashrc
On each machine, load resolves a tracked path as os/<this-os>/ → else files/ → else skip. So on Linux, .zshrc (which lives only in os/darwin/) is simply ignored, and vice-versa. You don’t annotate anything — the directory a file lands in (decided by add) is the only signal.
For shared shell snippets that need OS-specific paths, envkit also generates ~/.config/envkit/os.<shell> with built-in variables:
export ENVKIT_OS="linux"
export ENVKIT_ARCH="amd64"
export ENVKIT_BREW_PREFIX="/home/linuxbrew/.linuxbrew"
8. Bootstrap a fresh Linux server
On a brand-new Linux box (the secret backend is pass, which envkit sets up for you):
brew install envkit # or download a release binary
git clone <your-store> ~/.local/share/envkit/store
envkit init --store ~/.local/share/envkit/store
envkit setup # generates a GPG key + initializes pass
envkit load # renders the Linux variant
envkit setup is idempotent and a no-op on macOS (the Keychain is always available). If pass isn’t installed, doctor/setup print the exact install command for your distro (apt/dnf/pacman/zypper/apk). The repo also ships scripts/bootstrap.sh that does all of the above in one shot.
How it works
Two repos. The app (this repo — the envkit binary, generic) and your store (a private git repo with your dotfiles). envkit finds your store via ~/.config/envkit/config.toml.
The manifest (envkit.toml) lists tracked files:
[[file]]
path = ".zshrc" # relative to ~
kind = "shell" # shell | dotenv | plain
secrets = ["GITHUB_TOKEN"] # declared secret keys (optional)
noscan = false # skip the undeclared-secret scan for this file (optional)
Two directions, never a sync:
| Command | Direction | |
|---|---|---|
| Save this machine to the repo | envkit backup | ~ → store |
| Restore the repo onto a machine | envkit load | store → ~ |
status shows, per file, whether the two sides are in-sync, differs, missing-local, missing-store, or other-os.
Secrets are declared per file, extracted to the OS keychain on backup (macOS Keychain via security; Linux via pass), and replaced with a placeholder in the repo. On load, shell files source a generated ~/.config/envkit/secrets.<shell> that calls envkit secret get — so no plaintext ever lands in your dotfiles or your repo. envkit secret get is fail-soft (a missing key prints a warning and returns empty, never breaking shell startup).
Safety: load won’t overwrite locally-changed files without --force; every overwrite is backed up to ~/.envkit-backups/; the undeclared-secret scanner blocks backup if it spots a likely-secret you didn’t declare (override with --allow-unmanaged or mark the file noscan).
Command reference
| Command | Purpose |
|---|---|
init --store <dir> [--no-scan] | point envkit at your store; scans ~ for common dotfiles unless --no-scan |
add <path>… [--from FILE] [--os|--shared] [--kind K] [--secret KEY…] | track one or more files (auto-routes shell rc files to the OS layer); --secret is single-file |
rm <path> | stop tracking a file (keeps the local copy) |
backup [paths…] [--dry-run] [--force-push] [-m MSG] [--allow-unmanaged] | ~ → store; --force-push also fetch+rebase+commit+push |
load [paths…] [--dry-run] [--force] | store → ~; --force overwrites locally-changed files |
status | per-file sync state + per-secret keychain presence |
diff [path] | diff a tracked file between ~ and the store |
list | list tracked files |
secret get|set|rm|list [KEY] | manage keychain secrets |
setup | prepare the secret backend (GPG + pass on Linux; no-op on macOS) |
doctor | check config, store, keychain, backup health |
migrate | one-time: turn existing ~ symlinks into real files and seed the store |
FAQ
Is there a sync command? No, by design. A single command would have to guess which side wins on a conflict. Use backup (machine → repo) and load (repo → machine); status shows you what’s out of sync.
Where are my secrets stored? In the OS keychain — macOS Keychain or Linux pass (GPG-encrypted). Never in the repo, never in plaintext on disk. The repo only contains placeholders.
Will load clobber a file I changed locally? No. It skips files whose local copy differs (with a warning) unless you pass --force, and it backs up anything it overwrites to ~/.envkit-backups/.
Does it work on a headless server? Yes — pass is headless-friendly, and envkit setup provisions it. envkit was built and tested against a real Linux VPS.
Windows? Not supported.
Releasing (maintainers)
A git tag triggers CI (goreleaser on a self-hosted runner) to cross-compile all four targets, publish a GitLab Release, and update the Homebrew tap formula:
git -c tag.gpgSign=false tag -a v0.1.1 -m "v0.1.1"
git push origin v0.1.1
# users then: brew upgrade envkit
License
MIT © Syed Hasibur Rahman
This is a snapshot generated from GitLab. For the live README, see the project page.