# Commands

> Every zc-workstation command, flag and exit code.

| Command | What it does |
| --- | --- |
| 
`plan`

 | 

Read live state and list what would change. The default.

 |
| 

`apply [FILE]`

 | 

Make the changes, then plan again to verify. With a saved plan, exactly those.

 |
| 

`status`

 | 

What is in place per target, without diffing. Targets for another system show as skipped.

 |
| 

`list`

 | 

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.

 |
| 

`add ITEM`

 | 

Declare a package or runtime in one target’s manifest, with why it is there. Installs nothing; `plan` and `apply` do.

 |
| 

`capture`

 | 

Print the manifest one target would declare to match this machine. Needs `-target`.

 |
| 

`validate`

 | 

Check the config offline.

 |
| 

`doctor`

 | 

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](security.html#_what_doctor_checks).

 |
| 

`upgrade`

 | 

Save what is outdated to the state folder, then `apply -upgrade`. The review skill reads the snapshot to say what moved from what to what.

 |
| 

`history`

 | 

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.

 |
| 

`drift`

 | 

Plan, and save how many changes are outstanding to `~/.local/state/zc-workstation/drift.json`, which ZCBar and SketchyBar show. `-last` prints the saved one; `-plist` prints a launchd agent that runs it every morning.

 |
| 

`ui`

 | 

The [dashboard](ui.html).

 |
| 

`mcp`

 | 

The MCP server on standard input and output.

 |
| 

`version`

 | 

The version.

 |

## Flags

<table class="tableblock frame-all grid-all stretch"><colgroup><col style="width: 25%;"> <col style="width: 75%;"></colgroup><tbody><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-config FILE</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock">Config file, or <code>-</code> for standard input.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-target NAME</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock">Limit to this target; repeat for several.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-out FILE</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>plan</code>: save the plan for <code>apply FILE</code>. <code>capture</code>: write the manifest to <code>FILE</code> instead of standard output.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-force</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>capture -out</code>: overwrite <code>FILE</code> if it exists.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-state LIST</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>list</code>: only these states, comma-separated, such as <code>outdated,not declared</code>.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-kind LIST</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>list</code>: only these kinds, comma-separated, such as <code>cask,mas</code>.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-adoc</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>list</code>: the declared packages and runtimes as an AsciiDoc page, one table per section.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-why TEXT</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>add</code>: one line on why the item is there.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-section NAME</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>add</code>: the manifest section to add it under; a new name starts a new section.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-json</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>plan</code>, <code>status</code>, <code>list</code> and <code>doctor</code>: machine-readable.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-exit-code</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>plan</code>: exit 2 for changes, 3 for manual-only.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-upgrade</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>plan</code>, <code>apply</code>: also upgrade outdated packages and runtimes, as if every <code>packages</code> and <code>tools</code> target said <code>upgrade: true</code>.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-only RESOURCE</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>plan</code>, <code>apply</code>: only the change to this resource, such as <code>brew:tmux</code>; repeat for several. A saved plan remembers <code>-upgrade</code> and <code>-only</code>.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>-yes</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock"><code>apply</code>: skip the confirmation prompt for deletions.</p></td></tr></tbody></table>

## Inventory

```console
$ 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 entry `zc-workstation` does 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](providers.html). 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.

```console
$ 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.

```ruby
# 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:

```console
$ 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.

```console
$ 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`, `cask` or `tap` for a Brewfile, the manager’s name for apt and dnf, `runtime` for 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 its `tap` line too.
    
-   A runtime is `name@version`; without a version it asks for `latest`.
    
-   In a manifest with sections, `-section` is required, and the error lists them. An item the manifest already declares is refused.
    
-   App Store apps need their id: write the `mas` line 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.

```console
$ 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:name` as `list` shows 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.

```console
$ 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:

```console
$ 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

<table class="tableblock frame-all grid-all stretch"><colgroup><col style="width: 25%;"> <col style="width: 75%;"></colgroup><tbody><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>0</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock">Nothing to do, or the apply converged and was verified.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>1</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock">Something failed.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>2</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock">With <code>-exit-code</code>: changes are outstanding.</p></td></tr><tr><td class="tableblock halign-left valign-top"><p class="tableblock"><code>3</code></p></td><td class="tableblock halign-left valign-top"><p class="tableblock">With <code>-exit-code</code>: nothing left but <code>MANUAL</code> items.</p></td></tr></tbody></table>

## JSON

```console
$ 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:

```text
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
```
