CLI¶
Everything patcherctl can do, one subcommand at a time.
patcherctl is the command-line interface of Patcher. Each section below covers one subcommand and the flags that shape its output. Every operation here has a library equivalent, see Library if you would rather script it in Python.
Generate reports, rank patch posture, and inspect drift straight from your terminal.
Schedule exports straight into other tools.
After a one-time setup, every subcommand uses your stored Jamf credentials.
Export¶
Pulling patch data out of Jamf and into formats you can actually share. By default, a single invocation writes the patch report in all four formats (Excel, PDF, HTML, and JSON). If you only need one or two, narrowing the output is one option away.
Options¶
--path,-p(required)Where to save the reports
--format,-fRestrict output to specific formats (
excel,pdf,html,json). Pass multiple times on the CLI--sort,-sSort reports by a column
--omit,-oSkip patches released in the last 48 hours
--date-format,-dPDF header date format (see Date format below)
--ios,-mInclude iOS device data in reports (see iOS device data)
--concurrencyMax concurrent Jamf API requests (Default: 5)
--device-details,-DPer-title device sheets in the Excel export (slower on large fleets)
--homebrew/--no-homebrewAlso match titles against Homebrew Cask; adds a
Homebrewcoverage column (see Homebrew matching)
Examples¶
$ patcherctl export --path ~/reports
$ patcherctl export --path ~/reports --format html --format pdf
$ patcherctl export --path ~/reports --sort "Released" --omit
$ patcherctl export --path ~/reports --ios --homebrew
Date Format¶
The PDF header date format defaults to Month-Day-Year (e.g. January 31 2026). Available options:
Option |
Example |
|---|---|
|
January 2026 |
|
January 31 2026 |
|
2026 April 21 |
|
16 April 2026 |
|
Thursday September 26 2013 |
Concurrency¶
Patcher fans out Jamf API requests in parallel, capped at 5 concurrent in-flight by default. Increase the cap for faster fetches on instances that can take the load, or lower it for tenants behind aggressive rate limiting.
Warning
Cranking concurrency too high can starve other workloads on your Jamf server. Stay at or below 5 unless you’ve coordinated with whoever owns the Jamf instance. See Jamf’s API scalability best practices.
iOS Device Data¶
Passing --ios appends iOS / mobile device data to the report so you can see what’s running on your fleet alongside the macOS patch coverage. Behind the scenes Patcher calls three Jamf APIs:
Pulls the IDs of all enrolled mobile devices.
Resolves each ID to its current OS version.
Fetches the latest released iOS/iPadOS versions from SOFA to determine version recency.
The aggregate appears in the report as a count of mobile devices on the latest OS. Useful for the same SLA / compliance reporting workflows that drive --omit and the recent-release analyze criterion.
Homebrew Cask Matching¶
Patcher matches each Jamf patch title against the Installomator-sourced slugs in the Patcher API catalog. Passing --homebrew widens catalog matching to Homebrew Cask’s catalog, which covers apps that carry no Installomator label.
The flag is off by default, so reports without it stay byte-for-byte unchanged. Homebrew matching rides on the same catalog pass as Installomator, so it has no effect when Installomator matching is turned off.
Disabling Installomator Matching¶
If Installomator-style matching doesn’t fit your environment, turn the catalog client off entirely. When disabled, no catalog calls are made and the install_label field on every PatchTitle stays empty.
$ defaults write \
~/Library/Application\ Support/Patcher/com.liquidzoo.patcher.plist \
enable_installomator -bool false
Analyze¶
Filter, rank, and trend patch data to surface the titles that need attention.
Two flavors: point it at a single Excel report for one-shot filtering, or trend across every cached dataset. Either way the goal is to tell you which titles are lagging and which are humming.
See also
For pairwise snapshot comparison (added/removed/changed titles between two specific points in time), see Diff.
By default, the analyze command works against the latest exported report. To analyze a different one, pass an explicit Excel path.
Criteria¶
Two criteria families drive analyze, used in different contexts.
Changed in version 3.0
The FilterCriteria and TrendCriteria enums and Analyzer dispatch wrapper were replaced with TitleFilter and TrendAnalysis classes. Each former enum value is now a method on the respective class, so library callers can do TitleFilter(titles).most_installed(top_n=10) directly. CLI strings (--criteria most-installed) and PatcherClient.analyze("most-installed", ...) still work, only the enum surface was removed.
most-installedSoftware titles with the highest number of total installations
least-installedTop N least-installed titles (default 5)
oldest-least-completeOldest patches with the lowest completion percent
below-thresholdTitles with completion below the configured threshold (default 70%)
recent-releasePatches released in the last week
zero-completionTitles with 0% completion
top-performersTitles with completion above 90%
high-missingTitles where missing patches are >50% of total hosts
installomatorTitles that match an Installomator label
patch-adoptionCompletion rates over time for each software title
release-frequencyFrequency of updates per software title
completion-trendsCorrelation between release dates and completion percentages
Tip
CLI criteria names are dash-flexible: most-installed and most_installed both resolve. Library method names use the underscore form (TitleFilter(titles).most_installed()).
Options¶
--criteria XFilter or trend criterion. Accepts dash or underscore form
--top-n NCap result size for top-N criteria. Ignored by
below-thresholdandzero-completion(those return all matching titles)--threshold XCompletion-percent cutoff for
below-threshold(Default 70.0)--excel-file <path>Operate on a specific Excel report rather than the latest cached one
--all-timeSwitch from single-report filtering to trend analysis across every cached dataset
--summary+--output-dir <path>Write an HTML version of the analysis alongside the printed table
Examples¶
$ patcherctl analyze --criteria most-installed
$ patcherctl analyze --criteria below-threshold --threshold 50.0
$ patcherctl analyze --criteria least-installed --top-n 5
$ patcherctl analyze --excel-file /path/to/report.xlsx --criteria most-installed
$ patcherctl analyze --all-time --criteria patch-adoption
$ patcherctl analyze --all-time --criteria release-frequency
$ patcherctl analyze --all-time --criteria completion-trends
Generating a Summary¶
Pass --summary along with --output-dir to write an HTML version of the analysis alongside the stdout table. Summary files follow the naming pattern patch-analysis-<date>.html (or trend-analysis-<criteria>.html for trend analysis).
$ patcherctl analyze \
--criteria below-threshold \
--threshold 80.0 \
--summary \
--output-dir ~/Reports
Tip
recent-release pairs well with SLA / compliance reporting. Pull all patches released in the last week to confirm coverage against a 7-day SLA.
Diff¶
Compare patch state across two points in time. Find what shifted, what regressed, and what’s new.
patcherctl analyze --all-time answers “how have things trended”; patcherctl diff answers “what changed between these two specific moments.” Pair it with a scheduled export (automation) and you have a paper trail of every patch-coverage change without standing up a separate observability stack.
Diff reuses the same ~/Library/Caches/Patcher/patch_data_*.pkl snapshots that drive Analyze, so it works against history Patcher has already been collecting; no extra opt-in.
How Snapshots Are Selected¶
Flag |
Meaning |
|---|---|
(none) |
Fetch live patch data, compare against the most recent cached snapshot. |
|
Live vs. the earliest cached snapshot inside the trailing window. |
|
Live vs. the earliest cached snapshot ever. |
|
Skip the live fetch; compare the two most recent cached snapshots. Combine with |
|
Two ISO dates (YYYY-MM-DD). Picks cached snapshots closest to each date. Implies |
--list-snapshots prints every cached snapshot’s timestamp and filename, then exits. Use it when you’re not sure what’s available.
Tip
Snapshot timestamps come from filesystem mtime, not from the timestamp embedded in the cache filename (which is 12-hour and ambiguous). If you back up or move cache files, preserve mtimes.
Options¶
--since <window>Trailing window. Accepts
Nd/Nh/Nw--all-timeEarliest snapshot ever. Mutually exclusive with
--since--between <from> <to>Two ISO dates. Cannot combine with
--since,--all-time, or--no-fetch--no-fetchCompare cached snapshots only
--list-snapshotsPrint cache contents and exit
--format text\|jsontext(default) prints a table.jsonemits a structuredDiffResultfor piping
Examples¶
$ patcherctl diff
$ patcherctl diff --since 30d
$ patcherctl diff --all-time
$ patcherctl diff --between 2026-04-01 2026-05-01
$ patcherctl diff --no-fetch --since 7d
$ patcherctl diff --since 30d --format json | jq '.version_bumps'
$ patcherctl diff --list-snapshots
Available cached snapshots (oldest → newest):
2026-04-01T09:14:02 patch_data_04-01-26_09-14-02.pkl
2026-04-15T09:13:55 patch_data_04-15-26_09-13-55.pkl
2026-05-01T09:14:11 patch_data_05-01-26_09-14-11.pkl
What Gets Compared¶
A title is changed if completion percent, hosts patched, total hosts, or latest version differ between the two snapshots. Released date and Installomator label changes are intentionally ignored. A TitleChange row carries both before/after values plus the deltas, so consumers don’t need to recompute.
Output Anatomy¶
Diff: snapshot-2026-04-01T09:14:02 → snapshot-2026-05-01T09:14:11
────────────────────────────────────────────────────────────
ADDED (3)
Title | Released | Hosts | Complete
----------------+-------------+-------+---------
Slack | Mar 14 2026 | 189 | 95.2%
Microsoft Teams | Apr 02 2026 | 174 | 88.1%
CHANGED (12)
Title | Complete % | Hosts | Version
--------+---------------+-----------+----------------------
Firefox | 72.1% → 91.4% | 142 → 180 | 138.0 → 139.0 (bump)
Chrome | 91.2% → 88.0% | 179 → 173 | 137.0
REMOVED (1)
Title | Last released | Hosts
-------------+---------------+------
Adobe Reader | Apr 01 2026 | 95
SUMMARY
Titles | 87 → 89
Unchanged | 74
Version bumps | 8
Avg completion Δ | +4.20pp
JSON output is a DiffResult dump; safe to feed directly to jq, yq, or any downstream Pydantic consumer.
Tip
Pipe --format json into a daily Slack post or a status page; the version_bumps count is a clean leading indicator for “did upstream releases land in our fleet this week.”
Drift¶
Find apps where upstream patching sources disagree on what “latest” means. The strongest signal Patcher’s stitched catalog can report.
Every catalog source independently reports a current version for each app: Installomator’s appNewVersion, Homebrew Cask’s version. Most of the time they agree. When they don’t, one source is probably silently stuck. The vendor moved their release artifact, the upstream label still finds the old location, and the tool keeps reporting the old version as latest indefinitely.
patcherctl drift surfaces these disagreements. Pair it with a weekly or monthly cadence and you’ll catch silent failures upstream tools can’t detect themselves.
Sources That Participate¶
Only sources that expose a stable per-app version string get compared:
Source |
Version field |
Participates? |
|---|---|---|
Installomator |
|
Yes |
Homebrew Cask |
|
Yes |
AutoPkg |
resolves at recipe run time, not in catalog |
No |
Mac App Store |
empirically negligible overlap with versioned sources |
No |
Jamf App Installers |
coverage indicator only |
No |
Versions are compared via packaging.Version (so 4.32 and 4.32.0 are treated as equal, only meaningful disagreement counts as drift). Unparseable strings (Cask’s date-style 2025-04-15, Installomator’s shell-expression $(curl ...)) get a case-insensitive string compare and a parsed_ok=False marker in the result.
Options¶
--slug <slug>Inspect a single app. Mutually exclusive with
--vendor/--source--vendor <vendor>Case-insensitive exact vendor match. List mode only
--source <source>Require this source to be one of the disagreeing sources. List mode only
--limit <N>Page size. Server caps at 1000 (Default 100)
--offset <N>Entries to skip before the page
--format <text|json>jsonemits a structuredDriftResponseorDriftEntry
Examples¶
$ patcherctl drift
$ patcherctl drift --slug slack
$ patcherctl drift --vendor Slack
$ patcherctl drift --source installomator
$ patcherctl drift \
--format json | jq '.entries[] | select(.leader == "homebrew_cask")'
What Gets Returned¶
Every catalog source independently reports a current version for each app (Installomator’s appNewVersion, Homebrew Cask’s version). When they disagree, one source is probably silently stuck. A DriftEntry carries the slug, name, vendor, every source’s reported version, and a leader/laggard pair (the highest and lowest parsed versions). Both are None when any version couldn’t be parsed, the raw versions are still in versions so you can render the disagreement without ordering it.
The list endpoint returns a DriftResponse with total_scanned (apps with at least two versioned sources), total_with_drift (the filtered count of disagreements), and the page of entries.
Reset¶
Controlling Patcher’s state granularly.
The reset command restores specific configurations in Patcher. By default a full reset clears everything and re-runs the setup wizard. You can also reset individual components (credentials, UI settings, or cached data) without touching the rest.
Note
Options are case-insensitive. full, Full, and FULL all work.
Options¶
fullCredentials, UI config, setup state, and cache, then re-runs the setup wizard
UIPDF report appearance (header / footer text, font, optional logo)
credsKeychain credentials (URL, Client ID, Client Secret), all of them or just one
cacheCached patch data under
~/Library/Caches/Patcher
Caution
A full credential reset prompts for all three values (URL, Client ID, Client Secret). Only run it if you have access to the new credentials, particularly if your environment doesn’t use SSO, or you originally relied on Patcher’s automatic setup wizard.
Examples¶
$ patcherctl reset full
Resets everything and re-runs the setup wizard.
$ patcherctl reset UI
Refreshes the appearance of generated reports (header / footer text or custom logos). Patcher will re-prompt for UI settings after the reset succeeds.
Reset all three credentials:
$ patcherctl reset creds
Or scope to a single credential by name (one of url, client_id, client_secret):
$ patcherctl reset creds --credential url
$ patcherctl reset cache
Removes all cache files from the cache directory.
See also
For more about cached data and where Patcher stores it, see Data Storage.