Providers
What each zc-workstation provider reads, what it runs, and what it leaves to you.
A provider reads the machine and returns actions. It never writes during a
plan. Each one also counts what is in place, for status, doctor and the
dashboard.
packages
Installs what a manifest declares. It never uninstalls: a package you drop from the manifest stays installed, because removing software is your call.
| Manager | Manifest | Reads installed | Installs with |
|---|---|---|---|
|
A Brewfile: |
|
|
|
One package per line, |
|
|
|
One package per line, |
|
|
|
The file |
|
|
On a machine with no apt package lists yet, such as a fresh install or a
container, apply runs sudo apt-get update before the first install.
For list and the Inventory page, versions and descriptions come from
brew info --json=v2 --installed and mas list on macOS,
dpkg-query on apt systems, and rpm -qa on dnf systems. winget items list
without them. If the details cannot be read, items still list, without
versions.
Newer versions and undeclared packages come from:
| Manager | Newer versions | Installed on purpose | Upgrades with |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
winget has neither yet. These reads are best effort: if one fails, items still list without it.
-
With
upgrade: true, the plan has oneUPDATEper outdated declared package, such asupgrade with brew, 3.7c to 3.8. Without it, nothing is upgraded. A manager that cannot report newer versions gets oneMANUALstep instead. -
capturewrites a Brewfile (tap,brew,caskandmaslines) or a package list from the "installed on purpose" reads. Dependencies are left out, since the manager pulls them in again. -
Taps are installed before the formulae that need them.
-
Other Brewfile entries, such as
vscodeorgo, areMANUAL: the plan names them, and you install them yourself. -
masentries areMANUALuntilmasis installed. -
If the manager itself is missing, the plan is one
MANUALstep to install it. -
An app installed outside the manager is not seen. Its cask is installed with
--adopt, so Homebrew takes over the app already there; if the app differs from the cask, Homebrew refuses, and you delete the app or remove the entry.
dotfiles
Applies a chezmoi source directory. Use chezmoi’s
naming (dot_zshrc, private_, executable_, .tmpl) and its templates
for per-machine differences. Its secrets support keeps passwords out of the
repository.
The plan is chezmoi status:
| chezmoi | Plan | Meaning |
|---|---|---|
|
|
The file is not there yet. |
|
|
The file differs from the source. |
|
|
The source removes it, with chezmoi’s |
|
|
A |
In the inventory each file names the app it configures: the folder under
.config, .local/share, Library/Application Support or AppData, else
the file’s own name (.zshrc is zsh, .gitconfig is git). A login item
under Library/LaunchAgents is launchd.
A file edited since chezmoi last wrote it says so in the plan:
changed outside chezmoi, apply overwrites it. Copy your edit into the source
first if you want to keep it.
Apply runs chezmoi apply --force for exactly the planned paths and their
parent directories, nothing else. --force stands in for chezmoi’s own
overwrite prompt, because you already reviewed the plan.
zc-workstation ships one source of its own, assets/theme: the ZoolCoder
colours for Ghostty, tmux, starship, SketchyBar, JankyBorders, bat, btop, eza,
yazi, lazygit and fastfetch, each with a dark and a light variant. Its shell
settings, such as BAT_THEME_DARK and FZF_DEFAULT_OPTS, sit in
~/.config/zoolcoder/theme.zsh. The source creates ~/.zshrc.local to load
it, once: an existing ~/.zshrc.local is left alone and needs the line itself,
and an existing .zshrc needs one line, source ~/.zshrc.local. After apply, reload
what is running: sketchybar --reload, ~/.config/borders/bordersrc, and
tmux source-file ~/.config/tmux/tmux.conf.
With mode: symlink, each file is a link into the source directory, so an
edit in place lands in the repository. A file chezmoi must render, such as a
template or a private file, is still copied.
tools
Installs the runtimes a mise config pins. Only the tools declared in that file count. Tools from your global mise config or a parent directory’s config are ignored.
-
The plan lists one
CREATEpertool@versionthat is not installed, such asjava@temurin-25. -
Apply runs
mise installin the config’s directory. -
In the inventory, each runtime shows the version mise resolved next to the one requested, such as
24.21.0forlts, and the newer versionmise outdatedreports. Versions installed but asked for by no config in this target, such as a global tool or an old node, show asnot declared. -
With
upgrade: true, the plan has oneUPDATEper outdated runtime. Apply runsmise upgradefor thattool@requestin the config’s directory, so a pin such as3.13moves only within3.13. -
capturewrites a[tools]table of every installed version, using the request when mise knows it. -
mise refuses to read a config it does not trust. The plan then shows a
MANUALstep: read the file, then runmise trust. -
doctorchecks that the shell runs each command from mise, not from another copy earlier onPATH, such as Homebrew’snode. The fix is to activate mise last in the shell profile.
desktop
Sets how a Mac looks: the desktop picture, the page the
WebViewScreenSaver screen saver
shows, and the accent colour. macOS only, so the target needs os: [darwin].
-
Wallpaper. macOS keeps one picture for every Space on every display, in its wallpaper store,
~/Library/Application Support/com.apple.wallpaper/Store/Index.plist. System Events sees only the Spaces in view, so the plan reads the store itself, with a small JavaScript for Automation script run byosascript. If any Space shows another picture, the plan has oneUPDATE. Apply sets the picture through System Events, rewrites every Space in the store, and restartsWallpaperAgentso the desktops redraw. The first run asks you to let your terminal control System Events. Another tool that also sets the wallpaper, such as a login agent, will undo this; turn it off. -
Accent.
accentis one ofblue,purple,pink,red,orange,yellow,greenorgraphite. The plan readsAppleAccentColorwithdefaults; apply writes it and the matchingAppleHighlightColor. Apps show the new colour once they restart; log out to see it everywhere. -
Screen saver. The page, with the fonts and images its CSS loads from its folder inlined, becomes one
data:URL. The plan compares it with the saver’s default page,WVSSDefaultAddressURLin the saver’sInfo.plist, read withplutil. If they differ, apply writes it there withplutiland signs the saver again, ad hoc, withcodesign. The saver shows its default page while its own list of pages, set in its Options, is empty. -
If WebViewScreenSaver is not installed, in
~/Library/Screen Saversor/Library/Screen Savers, the plan is aMANUALstep: runassets/screensaver/build-saver.sh. It builds the saver from source with Xcode and installs it for you. The Homebrew cask is disabled, because the release build fails the Gatekeeper check; a local build is not quarantined. -
Choosing WebViewScreenSaver as the screen saver is yours to do, once, in System Settings, Screen Saver. macOS keeps that choice where no tool should write it.
-
A file that does not exist, such as a wallpaper not rendered yet, is a
MANUALstep.
The saver runs sandboxed: it cannot read files outside its bundle, and macOS
keeps its preferences in a container no terminal may open. That is why the
page travels inside the saver itself. Query options such as ?light do not
reach a data: URL, so the saver shows the page’s default, dark look.
settings
Keeps preferences at the values the config declares: on macOS the keys
defaults reads and writes, on GNOME the keys of gsettings, and on Windows
the user’s registry values. A target drives one system: os: [darwin] with
defaults, os: [linux] with gsettings or os: [windows] with
registry.
- name: macos
provider: settings
os: [darwin]
defaults:
NSGlobalDomain:
KeyRepeat: 2
NSWindowResizeTime: 0.001
com.apple.dock:
autohide: true
com.apple.screencapture:
location: ~/Screenshots
-
The plan reads each key with
defaults read. A key that is not set is aCREATE; one with another value is anUPDATE. Apply writes it with the type of the YAML value:-bool,-int,-floator-string. -
The Dock, Finder and the screenshot settings are read only when their process starts, so a change to
com.apple.dock,com.apple.finderorcom.apple.screencaptureadds an action that restarts it. -
A key the config stops declaring keeps its value. Some keys need a logout to take effect.
On GNOME, gsettings maps a schema to its keys. A dark desktop with the
dark Adwaita theme, a faster key repeat and the dock’s apps:
- name: gnome
provider: settings
os: [linux]
gsettings:
org.gnome.desktop.interface:
color-scheme: prefer-dark # prefer-light or default for a light one
gtk-theme: Adwaita-dark # Adwaita for a light one
org.gnome.desktop.peripherals.keyboard:
delay: 250
repeat-interval: 20
org.gnome.shell:
favorite-apps: [org.gnome.Nautilus.desktop, com.mitchellh.ghostty.desktop]
-
The plan reads each key with
gsettings getand compares values whatever the type gsettings prints, souint32 250matches250. -
Apply runs
gsettings setwith the value as GVariant text: a string is quoted, a list becomes an array of strings. GNOME applies a change at once; nothing restarts. -
A schema or key GNOME does not know is a
CREATEwhose apply fails with gsettings' message.
On Windows, registry maps a key under HKCU to its values. Dark mode, file
extensions shown and the taskbar on the left:
- name: windows
provider: settings
os: [windows]
registry:
'HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize':
AppsUseLightTheme: false
SystemUsesLightTheme: false
'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced':
HideFileExt: 0
TaskbarAl: 0
-
Only keys under
HKCU(orHKEY_CURRENT_USER): the user’s own settings, which need no administrator. Quote a key in single quotes, so YAML keeps its backslashes. -
The plan reads each value with
reg query. A bool or an integer is aREG_DWORD, sofalsematches0x0; a string is aREG_SZ. A string starting with~/is a path in your home folder. -
Apply runs
reg addwith the type. Windows applies most values at once; Explorer reads some only when it starts, so sign out to see them.
services
Keeps background services running: Homebrew services, launchd agents on
macOS, systemd user units and timers on Linux, and Task Scheduler tasks on
Windows. A target drives one system, os: [darwin], os: [linux] or
os: [windows]. It starts what is stopped and never stops anything.
- name: services
provider: services
os: [darwin]
services: [sketchybar, borders, colima]
agents: [~/Library/LaunchAgents/local.weekly-update.plist]
-
A service is in place when
brew services listshows itstarted, orscheduledfor one that runs on a timer. Otherwise apply runsbrew services restart, which starts it at every login and recovers one that failed. -
An agent is in place when
launchctl print gui/<uid>/<label>finds it; the label is the file name without.plist. Otherwise apply loads it withlaunchctl bootstrap. The plist can come from adotfilestarget listed earlier in the config, which writes it first. -
A changed plist is not reloaded. Reload it from a chezmoi
run_onchange_script that hashes the plist.
On Linux, agents names .service and .timer files:
- name: services-linux
provider: services
os: [linux]
agents: [~/.config/systemd/user/backup.timer, ~/units/syncthing.service]
-
A unit is in place when
systemctl --user showreports it enabled and active; a timer is active while it waits for its next run. -
Otherwise apply runs
systemctl --user daemon-reload, thensystemctl --user enable --nowwith the file’s path, which links in a unit that lives outside systemd’s folders. -
servicesworks on Linux too: Homebrew on Linux runsbrew serviceson systemd.
On Windows, agents names Task Scheduler task files, the .xml that Task
Scheduler’s Export or Export-ScheduledTask writes:
- name: tasks
provider: services
os: [windows]
agents: [tasks/weekly-update.xml]
-
The task’s name is the file name without
.xml. It is in place whenGet-ScheduledTaskfinds it and it is not disabled; PowerShell reports the state in English whatever the system language. -
A missing task is registered with
Register-ScheduledTaskfrom the file, in whatever encoding it has. A disabled one is enabled. -
A changed file is not registered again. Re-register it by hand, or from a chezmoi
run_onchange_script that hashes the file.
keyboard
Remaps keys on Linux with keyd, which works
under X11, Wayland and the console alike. The target installs a keyd config
as /etc/keyd/default.conf and keeps the keyd service enabled and running.
Linux only, so the target needs os: [linux].
- name: keys
provider: keyboard
os: [linux]
file: assets/keyd/default.conf
assets/keyd/default.conf makes Caps Lock Escape when tapped and Hyper
(Control, Alt, Shift and Super together) when held, as Karabiner does on the
Mac. Point file at your own config for other remaps.
-
keyd itself comes from your packages:
keydon Debian 13 and Ubuntu 25.04 or later, which name the programkeyd.rvaiya, or the keyd COPR on Fedora. Until it is installed the plan shows aMANUALstep. -
The plan compares the file byte for byte. Apply copies it with
sudo install -m 644, then runskeyd reloadwhen keyd is running. -
keyd is in place when
systemctl show keydreports it enabled and active; otherwise apply runssudo systemctl enable --now keyd.
agents
Declares, once, what every AI coding agent gets: MCP servers, a rules file, skills, and the models Ollama serves. It writes them into Claude Code, Codex, Cursor and Gemini CLI. It adds and updates, and never removes a server, skill or model the file does not name.
- name: ai
provider: agents
file: agents.yaml
agents.yaml:
clients: [claude, codex, cursor, gemini] # leave out for all four
servers:
context7:
command: npx
args: [-y, "@upstash/context7-mcp@2.1.0"] # pin a version; doctor warns otherwise
github:
url: https://api.githubcopilot.com/mcp/
headers: {Authorization: "Bearer ${GITHUB_TOKEN}"}
rules: AGENTS.md # relative to agents.yaml
skills: [skills/review] # folders holding a SKILL.md
models: [qwen3:8b, nomic-embed-text]
| Client | MCP servers | Rules | Skills |
|---|---|---|---|
claude |
|
|
|
codex |
|
|
|
cursor |
|
kept in Cursor’s settings |
none |
gemini |
|
|
none |
-
A server is set by name in
mcpServers; every other key in the file stays as it was. Each file is replaced in one rename, read again just before the write, so an agent writing it in between keeps its change. -
Codex reads TOML. Its servers live in a marked block of
config.toml. A server Codex already declares outside the block is aMANUALstep, since a table declared twice breaks the file. An env value written${NAME}becomes anenv_varsentry, which Codex passes through from your shell. -
The rules file lands in a marked block, so what you wrote around it stays.
-
A skill is in place when every file in its folder matches.
-
A model is in place when
ollama listshows it; otherwise apply runsollama pull.ollama servemust be running.listshows each model. -
Keep keys out of the file: write
${NAME}and keep the value in the keychain or a password manager.doctorwarns about plain-text keys.
localdir
Keeps a directory holding the files written inline in the config. Writes go
through os.Root, so a symlink or .. cannot lead a write outside the
directory. With prune, files the config does not list are deleted, after
you confirm.
It is useful for a handful of small files, and it is the worked example for writing your own provider.
Edit this page Last updated