Commands
Every zc-workstation command, flag and exit code.
| Command | What it does |
|---|---|
|
Read live state and list what would change. The default. |
|
Make the changes, then plan again to verify. With a saved plan, exactly those. |
|
What is in place per target, without diffing. Targets for another system show as skipped. |
|
Every package, app, dotfile and runtime the config declares, one line each: its state, installed version, the app it configures, and why it is there. Then what is installed but declared nowhere. |
|
Declare a package or runtime in one target’s manifest, with why it is there. Installs nothing; |
|
Print the manifest one target would declare to match this machine. Needs |
|
Check the config offline. |
|
Machine health: binary, system, config, the programs targets need, and each target. It also warns about weak security settings, keys without a passphrase, unsigned commits, secrets in the shell history and risky AI agent configs, each with its fix. Every check. |
|
Save what is outdated to the state folder, then |
|
What was applied, newest first: when, which target, from the terminal, the dashboard or an agent, and whether it worked. Drift changes show here too. |
|
Plan, and save how many changes are outstanding to |
|
The dashboard. |
|
The MCP server on standard input and output. |
|
The version. |
Flags
|
Config file, or |
|
Limit to this target; repeat for several. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Inventory
$ zc-workstation list
TARGET KIND NAME STATE VERSION APP ABOUT
brew brew starship in place 1.26.0 - Cross-shell prompt for astronauts
brew brew tmux outdated 3.7c -> 3.8 - Terminal multiplexer
brew cask ghostty in place 1.3.1 Ghostty Terminal emulator that uses platform UI and GPU
brew cask google-chrome missing - - -
brew brew jq not declared 1.8.1 - Lightweight and flexible command-line JSON processor
dotfiles dotfile .config/ghostty/config in place - ghostty -
runtimes runtime node in place 24.21.0 - requested lts
States:
-
in place: installed, and nothing newer is known. -
outdated: installed, and the manager has a newer version. A pinned formula never shows as outdated. -
missing: declared, not installed. -
differs: a dotfile that does not match its source, or a script still to run. -
manual: an entryzc-workstationdoes not install. -
not declared: installed on purpose, but no target declares it. For brew these are formulae installed on request, casks, taps and App Store apps; a dependency never shows.
ABOUT is the item’s why, from its manifest line, else the package manager’s
description; JSON has both, as why and desc, with the section. Versions
and descriptions come from the package manager; see
Providers. A target for another system is listed as
skipped, one that cannot be read with its error. The reads run side by side,
so a full list takes about two seconds on a Mac with a few hundred packages.
$ zc-workstation list -state outdated
$ zc-workstation list -state "not declared" -kind cask,mas
Why each item is there
A manifest says what is installed; its comments say why. A comment at the end of an entry’s line is that entry’s why. A comment line that opens a block, after a blank line or at the top of the file, is the heading of the section below it.
# Git
brew "git-delta" # Side-by-side, syntax-highlighted git diffs
brew "lazygit" # Git in a terminal UI, opened from tmux with prefix g
The same works in apt and dnf package lists and in a mise config’s
[tools] table: go = "1.26" # Builds zc-workstation.
list, the dashboard and the MCP list tool show the why. list -adoc
turns it into a page that names every tool, links its homepage, and says why
you have it; keep that page in your config repository and generate it again
after a change:
$ zc-workstation list -adoc > docs/software.adoc
Add
add writes one entry, with its why, into a target’s manifest. It installs
nothing: plan and apply do, so you review the change like any other edit.
$ zc-workstation add -target packages -section Git -why "Fixup commits that find their target" brew:git-absorb
added brew:git-absorb to /Users/me/config/Brewfile; review and install it with: zc-workstation plan -target packages
$ zc-workstation add -target runtimes -why "Builds zc-workstation" runtime:go@1.26
-
The item is
kind:name:brew,caskortapfor a Brewfile, the manager’s name for apt and dnf,runtimefor mise. A bare name is a formula, a package or a runtime. -
A formula or cask from a third-party tap, such as
brew:cirruslabs/cli/tart, brings itstapline too. -
A runtime is
name@version; without a version it asks forlatest. -
In a manifest with sections,
-sectionis required, and the error lists them. An item the manifest already declares is refused. -
App Store apps need their id: write the
masline yourself. winget files are not edited.
Remove
remove uninstalls one item now and drops its line from the manifest, so the
next plan does not bring it back. Apply only ever adds; remove is how
something leaves.
$ zc-workstation remove -target packages brew:htop
removed brew:htop and dropped it from /Users/me/config/Brewfile
$ zc-workstation remove -target packages mas:409201541
-
The item is
kind:nameaslistshows it. An item the manifest does not declare is still uninstalled, so leftovers go through the tool too. -
An App Store app that is not declared is named by its id.
Capture
capture writes the manifest that would declare what the machine has: a
Brewfile for brew, one package per line for apt and dnf, and a [tools]
table for mise. Use it to start a config from a machine you already set up,
or to see what a manifest misses.
$ zc-workstation capture -target brew > Brewfile.new
$ zc-workstation capture -target brew -out Brewfile.new
captured brew to Brewfile.new
It writes standard output unless you pass -out, and it refuses to replace a
file without -force. Diff the result against your manifest and copy what
you want; a capture lists everything installed on request, including what
you only tried once. winget has its own: winget export -o FILE.
Installs ask in the terminal
Apply runs installers attached to your terminal, so sudo, a license prompt
or an App Store sign-in can ask you. Their output goes to standard error, so
-json output stays parseable.
With no terminal, as under an AI agent or launchd, sudo asks for your
password in a macOS dialog instead, through SUDO_ASKPASS; Touch ID still
comes first when sudo is set up for it. A SUDO_ASKPASS you set yourself
is left alone.
Deletions ask first
A plan with a DELETE stops and asks you to type yes. Without a terminal,
and without -yes, apply is cancelled. Agents never get -yes.
Drift watch
drift -plist prints a launchd agent that runs drift at 9:00 (-hour
moves it). Keep it in your dotfiles and loaded by a services target, like
any other agent:
$ zc-workstation drift -plist > Library/LaunchAgents/dev.zoolcoder.zc-workstation.drift.plist
The agent carries the PATH of the shell that printed it, since launchd
starts agents with almost none. It only plans; nothing is applied. A change
in the count is written to history, and SketchyBar’s drift item shows the
count while it is above zero.
Exit codes
|
Nothing to do, or the apply converged and was verified. |
|
Something failed. |
|
With |
|
With |
JSON
$ zc-workstation plan -json
{
"changes": 1,
"manual": 1,
"actions": [
{
"op": "CREATE",
"target": "brew",
"resource": "cask:ghostty",
"detail": "install with brew",
"manual": false
},
{
"op": "MANUAL",
"target": "runtimes",
"resource": "trust",
"detail": "mise does not trust /Users/me/dotfiles/mise.toml; review it, then run mise trust /Users/me/dotfiles/mise.toml",
"manual": true
}
]
}
Reference
The usage text, as zc-workstation help prints it:
zc-workstation sets up this machine from a YAML file: packages, dotfiles
(chezmoi) and runtimes (mise), on macOS, Linux and Windows.
Usage:
zc-workstation plan [flags] show what would change (default)
zc-workstation apply [flags] [file] make the changes; with a saved plan, exactly those
zc-workstation status [flags] what is in place right now, per target
zc-workstation list [flags] every package, dotfile and runtime, with versions
zc-workstation capture -target t write t's manifest from what this machine has
zc-workstation validate [flags] check the config offline
zc-workstation doctor [-json] health: system, config, programs, targets
zc-workstation upgrade [flags] save what is outdated, then upgrade it
zc-workstation history [-n 20] [-json] what was applied, newest first
zc-workstation drift [-last] [-json] count what differs from the config, and save it;
-plist prints a launchd agent that runs it daily
zc-workstation add -target t -why w [-section s] kind:name
declare an item in t's manifest; plan and apply install it
zc-workstation mcp stdio MCP server for agents
zc-workstation remove -target t kind:name
uninstall an item and drop it from t's manifest; mas:ID for an undeclared app
zc-workstation ui [-addr a] the admin page, on loopback only
zc-workstation version
Exit codes:
0 nothing to do, or the apply converged and was verified
1 something failed
2 with -exit-code: changes are outstanding
3 with -exit-code: nothing left but MANUAL items
Flags:
-config string config file, or "-" for standard input; default: $ZC_WORKSTATION_CONFIG,
then ./zc-workstation.yaml, then ~/.config/zc-workstation/zc-workstation.yaml
-target value limit to this target; repeat for several
-yes skip the confirmation prompt for deletions
-exit-code plan: exit 2 for changes, 3 for manual-only
-upgrade plan and apply: also upgrade outdated packages and runtimes
-only value plan and apply: only the change to this resource, such as
brew:tmux; repeat for several
-json plan, status and list: machine-readable
-out string plan: save the plan, to apply exactly what you reviewed;
capture: write the manifest to this file, not stdout
-force capture: overwrite the -out file
-state list list: only these states, comma-separated (missing,outdated,...)
-kind list list: only these kinds, comma-separated (brew,cask,dotfile,...)
-adoc list: the declared packages and runtimes, with why each is
there, as an AsciiDoc page
Edit this page Last updated