← All projects

envkit

Portable env/dotfile manager with keychain-backed secrets

  • Go 97.25%
  • Shell 2.75%
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 ~/.zshrc stays 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 .zshrc and your Linux box its .bashrc, while sharing .gitconfig between them.
  • ↔️ No magic sync. Two explicit directions — backup (machine → repo) and load (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 marked kind = 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:

CommandDirection
Save this machine to the repoenvkit backup~ → store
Restore the repo onto a machineenvkit loadstore → ~

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

CommandPurpose
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
statusper-file sync state + per-secret keychain presence
diff [path]diff a tracked file between ~ and the store
listlist tracked files
secret get|set|rm|list [KEY]manage keychain secrets
setupprepare the secret backend (GPG + pass on Linux; no-op on macOS)
doctorcheck config, store, keychain, backup health
migrateone-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.