zenops
Declarative system configuration management for shell config and dotfiles.
zenops reads a TOML config from ~/.config/zenops/config.toml and uses it to
keep your shell environment, aliases, dotfile symlinks, and $PATH in sync
with what you've declared. Run zenops apply to make the system match the
config; run zenops status to see what would change without touching anything.
Getting started
If you already have a zenops config repo:
zenops init clones the repo into ~/.config/zenops/ and validates that
it has a config.toml. Authentication (SSH key, HTTPS credential helper)
uses whatever git is already configured to use. Pass zenops init <url> --apply to chain straight into zenops apply.
Starting from scratch
If you don't have a config repo yet, create ~/.config/zenops/config.toml
by hand:
[]
= "bash"
[]
= "hx"
[]
= "ls -la"
# A pkg is a tool zenops knows about: how to install it, how to detect it,
# which shell hooks it needs, and which dotfiles it owns. Configs live on
# the pkg that owns them, so `zenops pkg` and `zenops apply` stay in sync.
# `enable` defaults to "on" — the pkg is expected to be present. Use
# `enable = "detect"` for silent-miss or `enable = "disabled"` to skip.
[]
= "modal editor"
= ["helix"]
= { = "which", = "hx" }
# Dotfiles for helix. The `.config` directory name defaults to the pkg key
# (`helix`), so these land at `~/.config/helix/`. Override with an explicit
# `name = "..."` when the pkg key and config dir differ — e.g. a pkg keyed
# as `neovim` whose dir is `nvim`.
[[]]
= ".config"
= "configs/helix" # relative path inside the zenops config repo
= [ # listed files are symlinked; others are generated
"config.toml",
"languages.toml",
"themes/onedark-boh.toml",
]
A pkg's configs (and its shell hooks) only apply when the pkg is considered
installed — matching detect strategies on the current host, or no detect
specified at all (right for config-only or PATH-only pkgs). enable defaults
to "on" — "I expect this pkg to be here." If detect misses, zenops apply
and zenops status emit <pkg> is missing — install with: … so you notice
the drift. Use enable = "detect" for tools you may or may not have (silent
miss), or enable = "disabled" to skip a pkg entirely.
Use type = "home" with dir = ".something" instead of .config for
dotfiles that live directly under ~/.
User identity
The [user] section holds your name and email — the single source of truth
for anything that needs them (git, SSH allowed-signer principals, and any
template string in the config):
[]
= "Ada Lovelace"
= "ada@example.com"
Both fields are exposed as template variables ${user.name} and
${user.email}, usable in any config value that supports ${...}
expansion.
SSH allowed signers
zenops can generate ~/.ssh/allowed_signers — the file git consults when
verifying SSH-signed commits (git config gpg.ssh.allowedSignersFile). Each
entry is either a github lookup (zenops fetches the user's SSH signing
keys from https://api.github.com/users/<username>/ssh_signing_keys via curl
— note this is a different endpoint from the .keys file used for SSH
authentication) or a fully manual line:
[[]]
= "github"
= "octocat"
= "octocat@github.com"
[[]]
= "manual"
= "bob@example.com"
= "ssh-ed25519"
= "AAAAC3NzaC1lZDI1NTE5AAAAIExampleKeyMaterial"
The file is regenerated on every zenops apply, so adding or rotating entries
in the config keeps the signers file in lockstep. github entries require
curl on PATH; if fetching fails, the run aborts — switch to manual for
offline stability. Pointing git at the file is handled automatically when
[git.signing] is set to type = "ssh" (see below).
Git global config
zenops manages ~/.gitconfig from the [git] section plus [user]. Name,
email, and — when configured — commit signing are written on every zenops apply.
Enable commit signing with [git.signing], tagged by backend. SSH-based
signing (git 2.34+):
[]
= "ssh"
= "~/.ssh/id_ed25519-github.pub"
Classic OpenPGP signing:
[]
= "gpg"
= "ABCD1234DEADBEEF"
Setting [git.signing] also sets commit.gpgsign = true and the matching
gpg.format. With type = "ssh" plus any [[ssh.allowed_signers]] entries
configured, gpg.ssh.allowedSignersFile points at the managed file
automatically.
Commands
zenops init <git-url>— clone an existing zenops config repo into~/.config/zenopsand validate it.--applychains intozenops applyafter a successful clone;--branchpicks a non-default branch or tag.zenops apply— apply the config (write generated files, create symlinks). Pass--pull-configtogit pull --rebasethe config repo first.zenops status— show what would change.--diffshows file diffs.zenops pkg— list configured packages and whether their dependencies are met.--allincludes disabled packages;--all-hintsshows every install hint.zenops repo <git-subcommand>— run a passthrough git command inside the zenops config repo.zenops completions <bash|zsh|fish|elvish|powershell>— print a shell completion script to stdout. Normally sourced automatically via the built-inzenopspkg; only needed for manual setup.
License
Dual-licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.