Declarative, idempotent system configuration for Ubuntu/Kubuntu — hand it a recipe, it makes the machine comply.
Project description
🧑🍳 totchef
Write down how your machine should be set up. Run one command. It complies.
totchef is a declarative, idempotent system configurator for Ubuntu/Kubuntu. You
write a recipe.toml — apt repos and packages, vendor CLIs, files in /etc,
shell setup, per-app tweaks — and totchef makes the system match it. Run it on a
fresh laptop to bootstrap; run it again next week to top up. It only touches what
has actually drifted, so re-runs are cheap and safe.
Your tiny line cook: hand it a recipe and it works the whole kitchen — apt packages, vendor CLIs,
/etc, per-app tweaks — plating only what isn't already done. "Yes, chef!"
totchef up
Install
totchef is distributed as a uv tool. If you don't
have uv yet:
curl -LsSf https://astral.sh/uv/install.sh | sh
Then install (and later upgrade) totchef:
uv tool install totchef
uv tool upgrade totchef
This drops a totchef command on your PATH in its own isolated environment — no
Python setup, nothing to pollute your system.
60-second quickstart
-
Write a
recipe.tomlin your current directory:[apt_pkg] packages = ["ripgrep", "fd-find", "btop"] [url.uv] url = "https://astral.sh/uv/install.sh" update_action = ["self", "update"]
-
Preview what would change — no root, no writes:
totchef plan -
Apply it (escalates to root only for the steps that need it):
totchef up
Run totchef up again any time. Already-satisfied steps report up-to-date and
are skipped.
How an up run works
What happens when you run totchef up?
flowchart TD
R["recipe.toml"] --> F["find and load<br/>(cwd → ~/.config → /etc)"]
F --> V["validate against<br/>cook schemas"]
V --> G["build dependency graph<br/>(topological order)"]
G --> Q{"step needs root?"}
Q -->|yes| Root["root cooks<br/>run one at a time"]
Q -->|no| User["user cooks<br/>run concurrently<br/>(privilege dropped)"]
Root --> S["system converged"]
User --> S
S --> Rep["report what changed"]
You launch totchef as yourself. It escalates to root only when a step needs it,
and drops back to your user for everything else — so a vendor installer that wants
your $HOME runs as you, while an apt transaction runs as root.
Writing a recipe
A recipe is a TOML file. Each section is handled by a cook — a small
manager for one domain. The section name picks the cook: [apt_pkg] is cooked by
the apt-package cook, [url.uv] by the URL-installer cook.
There are two section shapes:
- Plain sections — one block of data, one unit of work.
[apt_pkg]with apackages = [...]list is a single step. - Subtable sections —
[url.uv],[url.rustup],[file.nvidia_power]— each named entry is its own step, scheduled independently.
Two reserved fields
Any entry may carry two fields that totchef reads and then strips before the cook
sees the rest:
| Field | Meaning |
|---|---|
needs_root |
true runs this step as root; otherwise it runs as you. Grant it on the leaf entry that needs it, never on a subtable header — that would hand root to every entry under it. Most cooks default sensibly (apt/snap are root, vendor installers are not). |
depends_on |
A list of steps that must finish first. Name an entry ("url.rustup"), a single-step section ("apt_pkg"), or a whole section ("apt_repo", which waits on all its entries). totchef topologically sorts the result; a cycle is a lint error. |
Any entry may also carry a pre_hook (a guard: a non-zero exit skips the step)
and a post_hook (a shell command run only when the step changed something) —
on a versioned section like [bun] these gate and follow the whole sync, on a
per-resource section like [file.<name>] they gate and follow each resource.
A temporary entry (a pinned workaround waiting on an upstream fix) may declare its
expiry: remove_when is a shell probe (exit 0 means "the thing this entry waits on
has happened") and remove_how the instruction for dismantling it. Every up and
plan evaluates the probes as the invoking user; a fired watch puts remove_how
in the end-of-run Action required block until the entry is deleted, while a
failing probe (no network, missing tool) just keeps waiting silently.
Section defaults
In a subtable section, keys set on the header are inherited by every entry: lists
union (the entry extends the shared list), scalars are overridden by the
entry. Handy for sharing a common depends_on or a base feature list:
[desktop]
depends_on = ["apt_pkg"]
features = ["VaapiOnNvidiaGPUs", "WaylandLinuxDrmSyncobj"]
[desktop.brave]
desktop = "/usr/share/applications/brave-browser.desktop"
features = ["AcceleratedVideoEncoder"] # → unions onto the two above
Validate before you run
totchef lint
Every entry is checked against its cook's schema (unknown keys are an error, not a
silent typo), the dependency graph is checked for cycles, and needs_root
placement is verified. totchef plan does all of that and then shows you the diff.
Built-in cooks
Run totchef cooks to see what's available on your machine. The ones that ship in
the box:
| Section | Cooks | Key fields |
|---|---|---|
[url.<name>] |
vendor curl | bash installers |
url, bin, args, update_action, update_guard |
[cargo] |
Rust crates via cargo-binstall |
packages |
[uv] |
Python CLI tools in isolated venvs | packages |
[bun] |
global npm packages via bun add -g |
packages |
[file.<name>] |
install a file with exact content | path, source or content, mode, pre_hook, post_hook |
[conf.<name>] |
own specific lines of a config file | target, line or lines, pre_hook, post_hook |
[bash.<name>] |
idempotent shell snippets | current_state, desired_state, apply, pre_hook, post_hook |
[apt_repo.<name>] |
third-party apt repos + keys (root) | key_url, uris, suites, components, architectures, pin_priority |
[apt_pkg] |
apt packages via nala (root) |
packages |
[snap] |
snap packages (root) | packages |
[desktop.<app>] |
.desktop Exec= overrides |
desktop, features, switches |
[chromium_flags.<app>] |
Chromium Local State / Electron argv.json |
local_state, argv_json, features |
[settings.<app>] |
merge an env block into a JSON file | settings_json, settings_env |
A full, real-world recipe — a hybrid-GPU laptop with an eGPU, NVIDIA drivers, and
browser tuning — lives in examples/totchef_recipe.toml.
A recipe repo
A recipe carries its assets and custom cooks beside it, all resolved relative to the recipe:
totchef_recipe.toml the recipe
totchef_files/ assets a [file]/[local_bin]/... entry installs via `source`
totchef_cooks/ loose <section>_cook.py plugins (highly custom cooks, e.g. chezmoi)
(Each also accepts the totchef.toml / totchef-recipe.toml and totchef-files / totchef-cooks spellings.)
Where totchef looks for the recipe
In precedence order:
--recipe PATH(or-r PATH) — a recipe file, or a repo directory holding one- a recognized recipe name (
totchef.toml,totchef_recipe.toml,totchef-recipe.toml) in the current directory, then walking up to/ - a recipe pinned by
totchef init(saved to~/.config/totchef/config.toml)
totchef where prints the path it would use, so you're never guessing.
Run totchef from anywhere
The example recipe's [bash.totchef] entry installs totchef to ~/.local/bin via
uv tool install, and its post_hook runs totchef init to pin the recipe. After one
totchef up, totchef up works from any directory.
Writing your own cook
totchef ships its built-in cooks through an entry-point group — and your cooks
join the same way. Two routes, depending on how much ceremony you want.
A packaged plugin (shareable, versioned)
Subclass a cook base in your own package and register the section it serves:
# my_totchef_plugins/foo_cook.py
from totchef.cook_base import PackageListCook
class FooCook(PackageListCook): ...
# your package's pyproject.toml
[project.entry-points."totchef.cooks"]
foo = "my_totchef_plugins.foo_cook:FooCook"
Install it alongside totchef and [foo] becomes a usable section:
uv tool install totchef --with my-totchef-plugins
A local cook (no packaging, instant)
Drop a <section>_cook.py into your recipe's totchef_cooks/ (carried with the recipe) or
into ~/.config/totchef/cooks/ (a per-machine hatch). It must define exactly one CookBase
subclass; the filename (minus a _cook/_root_cook suffix) is the section it serves. A
local cook shadows a built-in of the same name — this is how the example recipe ships
chezmoi.
The two cook shapes
VersionedCook— for things with versions (packages). You implementlist_requested/list_installed/find_latest/sync.StateCook— for desired-state resources (files, settings). You implementget_current_state/get_desired_state/apply_resource.FileStateCookdiffs by content hash for you.
Cooks only probe and act — they hold no diff logic. totchef owns every
decision about what changed and what to run.
Commands
| Command | What it does |
|---|---|
totchef up |
Apply the recipe; converge the system to it (escalates to root). |
totchef plan |
Dry-run: probe and print what would change. No root, no writes. |
totchef lint |
Validate the recipe against the cook schemas and exit. |
totchef cooks |
List every available cook and the section it serves. |
totchef where |
Print the recipe path that would be used. |
totchef --version |
Print the version. |
All recipe commands accept --recipe/-r PATH.
Every run also writes a full log to ~/.local/state/totchef/logs/totchef-<timestamp>.log (honors $XDG_STATE_HOME).
Set TOTCHEF_INLINE=1 to run every cook in the foreground — no fork, no sudo — with logs streamed straight to the terminal. Use it to debug a cook or to apply under an existing root shell.
One thing to know: convergence is create/update only
totchef drives resources toward their desired presence — it never prunes.
Removing an entry from your recipe (or uninstalling its target) leaves prior
artifacts in place: a written /etc drop-in, a repo's keyring, a .desktop
override. Teardown is deliberate and manual, so a typo can't wipe your system.
Development
totchef lives in the zyplux monorepo as
apps/totchef, with its tests under tests/totchef/. From the zyplux repo root:
| Command | What it does |
|---|---|
just c |
Full gate across every package: install, knip, typecheck, lint, test. |
just totchef |
Apply the example recipe (apps/totchef/examples/totchef_recipe.toml) to this machine. |
uv run totchef plan --recipe apps/totchef/examples/totchef_recipe.toml |
Dry-run the example recipe. |
uv run pytest tests/totchef |
Run just totchef's tests. |
License: MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file totchef-0.0.1.tar.gz.
File metadata
- Download URL: totchef-0.0.1.tar.gz
- Upload date:
- Size: 55.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49179bdb8ab0f4eab3d56f8753ee927c724ed98fff58c591d25673bf15f6f82a
|
|
| MD5 |
994cc59149af95f5c5e0ff43c1d41820
|
|
| BLAKE2b-256 |
3035568fda324794bfc248690e84fe878186ba168aec87a61bf5f86a06f11652
|
File details
Details for the file totchef-0.0.1-py3-none-any.whl.
File metadata
- Download URL: totchef-0.0.1-py3-none-any.whl
- Upload date:
- Size: 73.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a69a8d3cef89873222ca6942d3c92f97b0d0da3e025fc51df0965db07fb81020
|
|
| MD5 |
b9ec1a8ad916937be7300ad21f85832c
|
|
| BLAKE2b-256 |
7cdf25bc0a3edd8d8d86f80badd5cf8a0bd01ed3026844b02798c7b201976cbb
|