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.

KindForSecret handling
shell.zshrc, .bashrcplaceholder plus a generated secrets.<shell> the file sources
dotenv.env-style filesKEY= placeholder, value in the keychain
plaineverything elseno secret extraction

Two directions, never a sync

CommandDirection
envkit backup~ → store
envkit loadstore → ~

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

CommandWhat it does
envkit init --store <path>Write ~/.config/envkit/config.toml pointing at your store
envkit setupPrepare the secret backend (GPG + pass) on Linux
envkit migrateOne-time: turn existing ~ symlinks into real files and seed the store

Tracking

CommandWhat 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 listList tracked files

Moving files

CommandDirection
envkit backup~ → store. Secrets go to the keychain
envkit loadstore → ~. Won't clobber local changes without --force
envkit statusPer-file state: in-sync, differs, missing-local, missing-store, other-os
envkit diff <path>Diff one tracked file between ~ and the store

Secrets

CommandWhat it does
envkit secret listList 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

CommandWhat it does
envkit doctorCheck 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

  • load refuses to overwrite a locally-changed file without --force
  • every overwrite is backed up to ~/.envkit-backups/ first
  • the undeclared-secret scanner blocks backup when it spots a likely secret you did not declare — override with --allow-unmanaged, or mark the file noscan
  • secret get is 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.