Declarative machine setup you can read before you run
kempt
A config repo is one kempt.toml plus the files
it references. kempt parses it, shows you a plan, and applies only
what you approve. The primitive set is closed and there is no exec/script escape
hatch — every action is a built-in, versioned primitive, so the whole plan is reviewable
before anything on your machine changes.
$ kempt plan plan · profile developer · darwin/arm64 packages.core ✓ install starship, atuin, ripgrep, jq (brew) ✓ symlink ~/.config/zsh → config/zsh packages.terminal ✓ git-clone ~/.tmux/plugins/tmux-resurrect packages.muster + install: schuettc/muster 0.7.1 (github-release) ✓ json-merge ~/.claude/settings.json ✓ service tools.muster.serve (launchd) ✓ verify muster · command-exists, version-current 1 changes, 61 ok, 0 blocked
what it's for
Machine setup today is imperative: a dotfiles installer, a bundle file, a shell script you
pipe into sh and hope. You run it and find out what it did afterward — there is
no honest way to read the full set of changes first, because the script decides as it goes.
kempt inverts that. The setup is declarative — a manifest of
the state you want, not a sequence of commands — so kempt can inspect the machine, compute
the difference, and show you the exact plan before it touches anything. Nothing changes until
you approve the diff.
The manifest describes the end state — this software present, these files linked, this service loaded — not the steps to get there. kempt derives the steps, and derives them against your machine as it actually is right now.
kempt plan is read-only. It prints every delta apply would make and changes
nothing. What you read is what apply does — there is no branch the plan can't see, because
every action is a known primitive.
There is no exec, no run, no script hook — not as a
discouraged option, but as an absent one. A manifest can only invoke kempt's built-in
primitives, so a plan can never hide an arbitrary command.
Every step computes current vs desired and no-ops on a match. Running apply twice is safe; converging a mostly-set-up machine is cheap; the plan is honest about how little is actually left to do.
the manifest
A config repo is a single kempt.toml and the config files it
points at. The manifest is TOML and declares spec = 1; a parser rejects spec
values it doesn't know rather than mis-parse a manifest from the future. Packages are made of
ordered steps and run in dependency (needs) order. Crucially, a manifest
describes what — the safety class of each action (software, files, read-only) lives in
kempt's own code, never in the manifest, so a config can't declare its own installs "safe."
[kempt] spec = 1 [packages.core] description = "shell, prompt, history, modern CLI tools" [[packages.core.install]] brew = { formulas = ["starship", "atuin", "ripgrep", "jq"] } apt = ["ripgrep", "jq"] [[packages.core.symlink]] from = "config/zsh" to = "~/.config/zsh" backup = true [profiles.developer] packages = ["core", "terminal", "muster"]
Profiles are named package sets — personas like developer or
minimal — not conditionals. The only conditional in the manifest is
only = { os, arch } on a package or a step, and it is always visible in plan
output.
primitives
Every action a manifest can take is one of the primitives below — that is the whole vocabulary. The set is deliberately closed: when a real capability gap appears, the answer is a new versioned primitive in kempt, not a script hook. That is what keeps a plan fully reviewable. Each primitive carries a fixed safety class — software, files, or read-only — assigned by kempt, not the manifest.
brew · winget · apt · npm · pi
Cross-platform backends; kempt selects the applicable ones at plan time. Backends are
additive — npm/pi install on any host with the runtime, alongside
the OS backend. npm and pi entries can be pinned as
name@version; kempt then converges to that exact version.
download
github-release templates the asset with {os}/{arch},
sha256-verifies against checksums.txt, and installs atomically. download
fetches from a domain distribution instead of GitHub, verifying a .sha256
sidecar via the same stage-and-atomic-move path.
service
git-clone pins a ref or branch. service renders a launchd plist
(macOS) or systemd --user unit (Linux) and does a cmp-before-reload, so
an unchanged service never restarts.
symlink · json-merge · toml-merge · line-in-file
symlink links a repo-relative source (backup = true moves a real
file to .bak first). json-merge and toml-merge do an
additive, idempotent deep merge; line-in-file ensures a line or block.
command-exists · http-ok · version-current
Read-only checks that touch nothing: is a command on PATH
(command-exists / command-exists-any), does a URL return 2xx
(http-ok), does a symlink point where it should, has a GitHub release drifted
(version-current).
— absent by design —
There is no primitive that runs an arbitrary command, and there is no plan to add one. A
notes field can surface post-install hints in plan output, but notes are
documentation only — they run nothing.
plan & apply
The core loop is two commands. kempt plan inspects the machine and
prints the deltas apply would make — read-only, it changes nothing.
kempt apply converges the machine to the manifest, applying only the changes you
approved (it prompts unless you pass -yes). Installs stage to a temp file and land
with an atomic mv; services are cmp-compared before reload so
unchanged ones never restart.
kempt plan
The read-only diff. Prints every delta with its primitive and safety class, and honors
only = { os, arch } visibly. Pass -os/-arch to
dry-plan for another platform without inspecting the local machine.
kempt apply -yes
Converges to the manifest, applying only approved changes. Atomic installs, and cmp-before-reload services so nothing restarts without cause.
kempt update · kempt refresh
update pulls the repo, self-updates the binary, and converges.
refresh recomputes pending changes and — with auto-apply enabled — applies
files-class changes only, never software.
kempt verify
Runs the manifest's declared read-only checks — the same verify steps that
appear in a plan — to confirm the machine still matches, touching nothing.
install
The installer downloads the latest release for your platform, verifies its
checksum fail-closed, and drops the kempt binary in ~/.local/bin
(override with KEMPT_INSTALL_DIR). Then point kempt at your config repo, read the
plan, and apply it.
# 1. install the binary (fail-closed checksum, installs to ~/.local/bin) $ curl -fsSL https://kempt.tools/install.sh | sh # or build from source: go install ./cmd/kempt # 2. point kempt at your config repo $ kempt init <repo-url> # 3. read the plan, then apply only what you approve $ kempt plan $ kempt apply
Binaries are served from the kempt.tools
domain under the download family-URL contract; GitHub releases remain as a durable
backing store. See the
spec for the manifest schema and the full primitive set.