The dashboard answers what is this Mac doing right now by being looked at. That is the wrong shape for a script, a scheduled job, or an agent, so Searoom 0.5.0 adds a command that answers the same question in JSON.
It is the same signed executable as the app, not a second binary. The command dispatch runs before any AppKit object exists, so a recognized command never creates a window, a status item, or a timer: it collects through the same code the dashboard uses, prints, and exits. There is no daemon, no port, no privileged helper, and no network request on any telemetry or documentation command.
Getting the command
It is already there. Searoom links the command the first time it runs, so searoom answers in a terminal without you having found a setting first. It creates one symlink at ~/.local/bin/searoom and, when that directory is not already reachable, one clearly marked block in ~/.zprofile that puts it on PATH. Nothing else is touched and nothing is elevated.
The profile block is there because nothing else reaches a stock PATH without asking for privileges: macOS ships /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin, and both /usr/local/bin and /etc/paths.d are owned by root. It is written only when no login file already mentions ~/.local/bin, and .zshrc, .zshenv, .bashrc and .profile are all read even though Searoom never writes to them, because that is where people usually set PATH.
Homebrew does it its own way. brew install emaitchess/searoom/searoom links searoom into Homebrew's bin directory, which is on your PATH already. The link is recreated on upgrade and removed on uninstall, and it is a symlink to the executable inside the app rather than a copy, so the code signature is untouched. Searoom recognises a command Homebrew provided and leaves it alone.
Turning it off, and putting it back. The Searoom CLI switch in Settings removes the link and the profile block, and remembers the choice, so the next launch does not quietly undo it. searoom install-cli and searoom uninstall-cli do the same from a terminal:
/Applications/Searoom.app/Contents/MacOS/Searoom install-cliThose two are the only commands in the whole set that write anything. install-cli refuses to overwrite a file that is not a symlink and refuses to replace a symlink pointing at a different app; uninstall-cli removes the link only while it still points at this app.
Or use neither. Every command works from the executable inside the bundle, with no link at all:
/Applications/Searoom.app/Contents/MacOS/Searoom status --prettyThe name the binary is invoked under is what selects the mode. As Searoom with no arguments it launches the menu-bar app; as searoom with no arguments it prints the command reference. A recognized command always runs the command.
The commands
Thirteen, and only one of them changes anything on disk.
| Command | Prints | Collects | Writes |
|---|---|---|---|
help [COMMAND] [--json] | The command reference, or the whole catalog as JSON | No | No |
version [--json] | App version, build, schema version, macOS floor | No | No |
sample [--interval N] [--pretty] | One fully primed telemetry sample | Yes | No |
watch [--interval N] [--count N] | One sample per line, as JSON Lines | Yes | No |
status [--interval N] [--pretty] | Pressure, limiting signals, headroom, sustained context | Yes | No |
history [--since] [--until] [--limit] [--jsonl] | Samples the app already persisted | No | No |
capabilities [--pretty] | What this particular Mac can actually report | Yes | No |
metrics [METRIC] [--json] | Canonical definitions, units, limits | No | No |
schema | The JSON Schema for every document above | No | No |
agent-guide | The bundled Agent Skill | No | No |
self-test | Collector and bundled-resource checks | Yes | No |
install-cli | Creates ~/.local/bin/searoom | No | Yes |
uninstall-cli | Removes it again | No | Yes |
--interval accepts a whole number of seconds from 1 to 10, defaulting to 2, and matches the app's own sampling range. --since and --until take either an RFC 3339 timestamp or a relative duration such as 15m.
Every option also has a one-letter shorthand, and both spellings are accepted everywhere: -i is --interval, -p is --pretty, -j is --json, -c is --count, -n is --limit, -s and -u are --since and --until, and -l is --jsonl. Standalone, searoom -h prints the command reference and searoom -v prints the version. Any command also takes -h, so searoom watch -h describes watch without leaving the line you were typing, and it wins over validating the rest of the arguments. The help --json catalog lists each option's shorthand beside its long name, so an agent can pick either form and read both from the same document.
The two legacy flags still behave exactly as they did before the CLI existed. --dump-sample emits the historical 41-field shape with integer pressure levels, unchanged and frozen, so anything already parsing it keeps working; --self-test still runs without a test framework. New work should use sample, which is the versioned document.
What a reading looks like
searoom sample --pretty, memory block only:
"memory" : {
"availableBytes" : 28971909120,
"cachedBytes" : 16365469696,
"compressedBytes" : 2452045824,
"compressionBytesPerSecond" : 0,
"compressionIOAvailability" : "available",
"pressureFraction" : 0.43787097930908203,
"pressureLevel" : "nominal",
"swapIOAvailability" : "available",
"swapInBytesPerSecond" : 0,
"swapOutBytesPerSecond" : 0,
"swapUsageAvailability" : "available",
"swapUsedBytes" : 191823872,
"systemPressureLevel" : "nominal",
"systemPressureSource" : "kern.memorystatus_vm_pressure_level",
"totalBytes" : 51539607552,
"usedBytes" : 22567698432,
"vmStatisticsAvailability" : "available"
}Three properties of that block are the whole contract in miniature. Every byte count is bytes, not a formatted string with a unit glued on. Every rate is per second. And every group carries its own availability, because whether a reading is trustworthy is a different question from what the reading says.
Timestamps are UTC RFC 3339 with milliseconds. Pressure levels are the lowercase strings nominal, elevated, constrained and critical; the integers the app persists internally are private and never appear here.
Nothing is fabricated
A reading that could not be taken is JSON null, with an availability value beside it saying why. This matters more than it sounds: a monitoring tool that reports zero for a sensor it could not read is not merely unhelpful, it is wrong in the direction that looks reassuring.
| Value | Meaning |
|---|---|
available | The reading was taken. A zero here is a real zero. |
warmingUp | A rate needs two counter reads and has only had one. Not an error; ask again. |
unavailable | The read failed, or this Mac has no such sensor. The value is null. |
legacyUnknown | A sample persisted before availability metadata existed. Its value may be real or may be a placeholder zero, and there is no way to tell after the fact. |
sample, watch, status and capabilities prime their rate baselines before emitting anything, discarding one collection and forcing a second disk-counter read, so the first document you get back carries real disk throughput rather than a structural zero. That priming is why a single sample takes about a second longer than you might expect.
sample, watch and status also carry sample.topProcesses: the five heaviest readable processes by CPU rate and the five by resident memory, refreshed at the requested interval like every other rate. A process CPU figure is CPU seconds per wall second and can exceed 1.0 when a process uses several cores, the same unclamped quantity as the observer metric. The priming pass reads the ranking once too, so the first document already carries real per-process rates. Sandboxed helpers and other-user processes that refuse inspection are omitted rather than shown as zero, because macOS keeps their CPU and memory figures private without privileges, and ps reads those only because it is setuid root; a column with no measurable consumers is empty rather than invented. The ranking is ephemeral: it never appears in history, because process names do not belong in the app's stored archives.
searoom capabilities answers the availability question for a specific Mac before you write anything against it, group by group, with the source and the refresh cadence for each. Direct temperature, fan speed and GPU utilization are the ones that vary by model and macOS release.
An instant, a window, or the past
Four commands collect, and they differ in what they are honestly able to claim.
sample is one moment. It cannot tell you whether anything is sustained, and it does not pretend to.
watch is a window you create. One compact document per line, on monotonic deadlines so a slow write does not make the interval drift. --count bounds it; otherwise it runs until interrupted, and both SIGINT and SIGTERM end it at a line boundary rather than mid-document, so the output stays parseable when you stop it.
searoom watch --interval 2 --count 30 > run.jsonlstatus is the interpretation. It names the limiting resource, the headroom, and the recent peak pressure. It reports sustained context only when the app's persisted history is fresh enough to support it, and says so when it is not — one sample is never presented as a sustained run.
history is the app's record, not the CLI's. It reads the same local archive the trend graphs draw from and collects nothing. That has one consequence worth stating plainly: history exists only for the time the menu-bar app was actually running. If you have never launched it, history is an empty success rather than an error. Live CLI samples are never written into it, so a script cannot pollute the app's trends.
Exit codes
They follow BSD sysexits, so a wrapper can tell a typo from a broken sensor.
| Code | Meaning |
|---|---|
0 | Success. An empty result is still success. |
1 | self-test found a failing check. |
64 | Usage: an unknown command, flag, or out-of-range value. |
65 | Data: a corrupt, unsupported or oversized history archive, or an install conflict. |
70 | Software: a bundled resource is missing or unreadable. |
74 | I/O: the filesystem refused a read or a link operation. |
130, 143 | watch ended on SIGINT or SIGTERM, at a line boundary. |
For agents
Everything an agent needs to use this correctly is on the machine already, and none of it requires a network request.
command -v searoom # is it installed, and where
searoom help --json # every command, option, default, exit code
searoom schema # the JSON Schema for every document
searoom metrics --json # 49 metric definitions, with limits
searoom capabilities # what this Mac can actually report
searoom agent-guide # how to read the output without overstating ithelp --json is the discovery entry point, and it is deliberately more than a usage string: each command carries its output document type, its exit codes, its valid argument values, and two booleans — collectsTelemetry and changesFilesystem — so a tool can decide what is safe to run unattended without parsing prose.
The same three contracts are published here, byte-identical to the copies inside the app, for a reader deciding whether to install it at all:
- The telemetry JSON Schema, which is the URL every document names in its own
$schemafield. - The metric catalog: identifier, JSON path, unit, range, nullability, source, derivation, thresholds, first-sample behaviour and limitations, for all 49 values.
- The Agent Skill, which is the same text
searoom agent-guideprints.
Searoom will also install its Agent Skill straight into the folders these agents read — Claude Code, Codex, Cursor and OpenCode, one at a time or all at once — from the Agent skills control in Settings. It writes one SKILL.md per agent and changes nothing else, and the same menu removes what it installed.
The schema is self-contained: it has no remote $ref, so a validator that has fetched the one file needs nothing else. Within version 1, new optional fields may be added; renaming a field or changing its type, unit, range or nullability requires a new schema version, which is what the schemaVersion field is for.
Limits, and what this is not
It is not a daemon or an API. Each invocation collects and exits. There is no socket, no port, no background process, and no way to reach it from another machine.
It does not talk to the running app. The CLI and the menu-bar app share code and one history file, not a connection. The CLI reads that file; it never writes it, and it never touches settings or launch-at-login state.
It is not a profiler. It reports what the machine has left and where the contention is, not which line of code caused it.
Some sensors are best-effort, and this is where that shows. Direct temperature, fan speed and GPU utilization come from interfaces whose availability varies by Mac model and macOS release. capabilities tells you which ones this Mac has before you build on them.
It is macOS 14 or later on Apple Silicon, like the app, and it is the same MIT-licensed source.
Where to go next
The metric reference gives the meaning and the limit of every value the commands emit, in prose rather than JSON. Everything Searoom does is the equivalent list for the app itself. The download page has the disk image, the Homebrew one-liner and the checksums.