envkit
Your dotfiles in git, your secrets in the OS keychain — on macOS and Linux.
envkit backs up the files that configure your machine to a git repository you control, and keeps the secrets inside them out of that repository entirely.
What makes it different
Most dotfile tools symlink your home files into a repo. That couples the machine to the repo: delete the repo and your shell breaks. And they have no answer for secrets, so people either commit tokens or maintain a pile of manual steps.
envkit takes a different stance.
Decoupled, not symlinked. Your ~/.zshrc stays a real file. The repository is
a backup you push to and pull from. If it vanishes, your machine keeps working.
Secrets never touch git. Declare which values are secret and envkit moves them into the OS keychain, leaving a placeholder in the repo. Your shell fetches them at startup.
One store, many machines. Per-OS layers give your Mac its .zshrc and your
Linux box its .bashrc from the same store, while sharing .gitconfig.
No magic sync. Two explicit directions — backup and load — so envkit never
guesses which side should win.
Where to start
New to it? Getting started takes you from nothing to a backed-up machine. Then Secrets, which is the part most tools get wrong.
Already running it and want a specific answer? Try the command reference or how it works.
Getting started
Install
The AiCE-Lab tap serves every tool we publish, so one tap covers envkit and
devlog both.
brew tap aice-lab/tap https://gitlab.com/aice-lab/homebrew-tap.git
brew install envkit
On Linux this also installs pass, which envkit uses as the keychain.
Create a store
The store is a private git repository holding your dotfiles. It is separate from envkit itself, and it is yours.
# on your git host, create an empty PRIVATE repo, then:
git clone git@gitlab.com:you/dotfiles.git ~/dotfiles
Point envkit at it
envkit init --store ~/dotfiles
envkit doctor
doctor should report the config and store as loadable. Run it whenever
something looks wrong — it checks the config, the manifest, the store's git
remote, and the keychain backend.
Track your first file
envkit add .zshrc
envkit status
status shows every tracked file as in-sync, differs, missing-local,
missing-store or other-os.
Back it up
envkit backup
cd ~/dotfiles && git push
envkit copies the tracked files into the store. By default it does not commit or push — the store is a normal git repository and stays under your control. If you want it to do the whole thing:
envkit backup --force-push -m "laptop dotfiles"
That fetches, rebases, commits and pushes. Both forms take --dry-run if you
want to see what would happen first.
Restore on another machine
brew tap aice-lab/tap https://gitlab.com/aice-lab/homebrew-tap.git
brew install envkit
git clone git@gitlab.com:you/dotfiles.git ~/dotfiles
envkit init --store ~/dotfiles
envkit load
load will not overwrite a locally-changed file without --force, and every
overwrite is backed up to ~/.envkit-backups/ first.
One thing load does not restore: your secrets. The keychain is per-machine.
See Secrets for how to move those across.
envkit doctor --secrets # which indexed keys have no value on this machine
Tracking files
The manifest
Tracked files are listed in envkit.toml inside your store.
[[file]]
path = ".zshrc" # relative to ~
kind = "shell" # shell | dotenv | plain
secrets = ["GITHUB_TOKEN"] # declared secret keys (optional)
noscan = false # skip the undeclared-secret scan (optional)
envkit add writes these entries for you; you can also edit the file by hand.
envkit add .zshrc --os # store under this OS's layer
envkit add .gitconfig --shared # share across every OS
envkit add .npmrc --secret NPM_TOKEN # declare a secret as you add it
envkit add --from paths.txt # newline-delimited list
Without --os or --shared, envkit picks based on the file: known shell files go
to the OS layer, everything else is shared.
Kinds
The kind decides how envkit handles secrets inside the file.
| Kind | For | Secret handling |
|---|---|---|
shell | .zshrc, .bashrc | placeholder plus a generated secrets.<shell> the file sources |
dotenv | .env-style files | KEY= placeholder, value in the keychain |
plain | everything else | no secret extraction |
Two directions, never a sync
| Command | Direction |
|---|---|
envkit backup | ~ → store |
envkit load | store → ~ |
There is deliberately no sync. A tool that guesses which side should win is a
tool that eventually guesses wrong with your only copy.
Stopping
envkit rm .zshrc # stop tracking; the local file is left alone
Secrets
This is the part most dotfile tools get wrong, so it is worth reading properly.
The rule
Secrets never touch git. You declare which values are secret; envkit moves them
into the OS keychain on backup and leaves a placeholder in the store. Your shell
fetches them at startup from the keychain, so no plaintext lands in your dotfiles
or your repository.
The keychain is macOS Keychain on darwin, and pass on Linux.
Declaring one
[[file]]
path = ".zshrc"
kind = "shell"
secrets = ["GITHUB_TOKEN"]
On the next backup, envkit lifts the value out of .zshrc, stores it in the
keychain, and writes a placeholder into the store copy. Your shell sources a
generated ~/.config/envkit/secrets.zsh that calls envkit secret get.
That generated file is never committed.
Managing values directly
envkit secret list # names only, never values
envkit secret get GITHUB_TOKEN
envkit secret set GITHUB_TOKEN # prompts, input hidden
envkit secret rm GITHUB_TOKEN
To store a value non-interactively, pipe it in — this keeps it out of your shell
history and out of ps:
printf %s "$VALUE" | envkit secret set GITHUB_TOKEN
Running a command with secrets injected
envkit exec GITHUB_TOKEN -- gh pr list
envkit exec GITHUB_TOKEN:GH_TOKEN -- some-tool # rename on the way in
Prefer this over export — the value exists only for that process.
secret get is fail-soft, and why that matters
A missing key prints a warning and returns an empty string with exit code 0. That is deliberate: a typo in a key name must not break shell startup and lock you out of your terminal.
The trap: an empty return looks exactly like a working call. If a script silently
gets "", check the key exists with envkit secret list before concluding the
store is broken.
A genuine keychain error — a locked keychain, a broken pass — exits non-zero,
so automation still fails loudly on real faults.
The undeclared-secret scan
On backup, envkit scans tracked files for things that look like secrets you did
not declare, and refuses rather than committing one by accident.
envkit backup --allow-unmanaged # override for this run
Or mark a file noscan = true if it trips the scanner repeatedly for good reason.
Moving secrets between machines
The keychain is per-machine, so load restores your files but not your secrets.
Set them on the new machine:
envkit secret set GITHUB_TOKEN
envkit doctor --secrets reads every indexed key and reports the ones with no
value, so you can see what still needs setting rather than discovering it when
something breaks. Plain doctor deliberately skips this: it costs one keychain
read per key and may prompt.
Many machines
One store, different operating systems
A Mac and a Linux box need different shell files but can share plenty else. envkit
handles this with OS layers inside the store: a shared layer, plus os/darwin
and os/linux.
A file tracked as OS-specific is restored only on that OS. A shared file — a
.gitconfig, say — goes to both. On the wrong OS, status reports other-os
rather than pretending the file is missing.
The os-layer copy wins over the shared copy for the same path.
Bootstrapping a fresh machine
brew tap aice-lab/tap https://gitlab.com/aice-lab/homebrew-tap.git
brew install envkit
git clone git@gitlab.com:you/dotfiles.git ~/dotfiles
envkit init --store ~/dotfiles
envkit load
envkit doctor --secrets # which secrets still need setting here
On Linux, run envkit setup first if pass has never been initialised there —
it prepares the GPG key and password store that back the keychain.
What does not travel
Secrets. The keychain is per-machine by design, so load brings your files and
leaves the values to you. envkit doctor --secrets reads every indexed key and
reports the ones with no value here — plain doctor skips that, because reading
every secret costs a keychain call each and can prompt.
This is the deliberate trade: a store that carried secrets would put every secret on every machine that clones it.
Command reference
Run envkit <command> --help for the full flag list; this is the map.
Setting up
| Command | What it does |
|---|---|
envkit init --store <path> | Write ~/.config/envkit/config.toml pointing at your store |
envkit setup | Prepare the secret backend (GPG + pass) on Linux |
envkit migrate | One-time: turn existing ~ symlinks into real files and seed the store |
Tracking
| Command | What it does |
|---|---|
envkit add <path> | Track a file, relative to ~. --from reads a list |
envkit rm <path> | Stop tracking. The local file is left alone |
envkit list | List tracked files |
Moving files
| Command | Direction |
|---|---|
envkit backup | ~ → store. Secrets go to the keychain |
envkit load | store → ~. Won't clobber local changes without --force |
envkit status | Per-file state: in-sync, differs, missing-local, missing-store, other-os |
envkit diff <path> | Diff one tracked file between ~ and the store |
Secrets
| Command | What it does |
|---|---|
envkit secret list | List managed key names. Never prints values |
envkit secret get <KEY> | Print a value. Missing key: empty + warning, exit 0 |
envkit secret set <KEY> [VALUE] | Store a value. Prompts if omitted; reads a pipe if not a terminal |
envkit secret rm <KEY> | Delete a value |
envkit exec KEY[:ENVNAME]... -- <cmd> | Run a command with secrets in its environment |
Checking
| Command | What it does |
|---|---|
envkit doctor | Check config, store, manifest, keychain, and leftover coupling |
Exit codes
0 success. Non-zero on a real failure — a keychain error, an unreadable store, a
backup blocked by the undeclared-secret scan.
The exception worth remembering: secret get on a missing key is exit 0 with
an empty value, so a typo cannot break shell startup. See Secrets.
How it works
Two repositories
The app — envkit itself, generic, installed from Homebrew — and your store,
a private git repository holding your dotfiles. envkit finds the store through
~/.config/envkit/config.toml.
Keeping them separate is the point. The store is data you own; envkit is a tool that reads and writes it.
The manifest
envkit.toml in the store lists what is tracked, each file's kind, and which of
its values are secret. It is plain TOML and safe to edit by hand.
Two directions
backup copies ~ → store. load copies store → ~. There is no sync, because
a sync has to decide which side wins, and getting that wrong costs you the only
copy of something.
status tells you where each file stands so you can choose.
Secrets
Declared secrets are extracted on backup into the OS keychain — macOS Keychain
through security, Linux through pass under an envkit/ namespace — and replaced
with a placeholder in the store. Shell files source a generated
~/.config/envkit/secrets.<shell> that calls envkit secret get at startup.
Because neither backend can list by service, envkit keeps its own index of managed
key names at ~/.config/envkit/keys.list. It holds names only, never values.
Safety rails
loadrefuses to overwrite a locally-changed file without--force- every overwrite is backed up to
~/.envkit-backups/first - the undeclared-secret scanner blocks
backupwhen it spots a likely secret you did not declare — override with--allow-unmanaged, or mark the filenoscan secret getis fail-soft on a missing key so a typo cannot lock you out of your shell, but exits non-zero on a real keychain error
Troubleshooting
envkit: command not found over SSH or in cron
~/.local/bin is on your interactive PATH but often not on a non-interactive
one, so a command that works when you log in fails from a script or a CI job.
command -v envkit || echo "not on this PATH"
Set the PATH explicitly in the unit, cron entry or script rather than relying on the login shell.
secret get returns nothing
A missing key prints a warning and returns empty with exit code 0 — by design, so a typo cannot break shell startup. It looks identical to success.
envkit secret list # is the key actually there?
envkit doctor # config, store and keychain in one pass
If the key is listed but the value is empty, the keychain is the place to look.
The keychain will not open
On Linux, pass needs an initialised GPG key:
envkit setup
gpgconf --list-dirs agent-socket # is the agent reachable from here?
In a non-interactive context the agent may be unable to prompt for a passphrase. That surfaces as a keychain error, which exits non-zero — unlike a missing key.
backup refuses to run
The undeclared-secret scanner found something that looks like a secret you have not declared. That is the feature working.
Either declare it in the file's secrets list, or:
envkit backup --allow-unmanaged
If a file trips it repeatedly for good reason, set noscan = true on that entry.
load skipped a file
load will not overwrite a file you have changed locally. Check with
envkit diff <path>, then envkit load --force if the store's copy should win.
The previous contents go to ~/.envkit-backups/ either way.
A file shows as other-os
It is tracked in the OS layer for a different operating system, so it is correctly not restored here. See Many machines.