Report
homebutler report collects the system, containers, processes and listening ports, compares them with the newest saved snapshot, and prints what changed. Run it from cron, by hand, or through an agent; the first run saves a baseline and every later run compares against the one before it.
homebutler report
| Flag | Default | What it does |
|---|---|---|
--keep <n> | 30 | Snapshots to keep after saving. Older ones are deleted. A value below 1 keeps one. |
--no-save | off | Prints the report and writes no snapshot. The next run still compares against the last saved one. |
--json | off | Prints the complete, ungrouped report as JSON. See Use the report from a script. |
Read a reportโ
A report has up to four sections, in this order: Needs Attention, Notable Changes, Current Status and Suggested Actions. The header names the snapshot the report compared against:
๐ Homebutler Report โ homelab
2026-10-25T18:14:03Z ยท compared with 2026-10-25T18:13:41Z
"What changed" means since that snapshot, not since a fixed interval. A report run hourly and one run monthly use the same kinds of line over very different spans, and the header is how you tell them apart.
Each line in Notable Changes has three columns:
- Kind: one of eight fixed words. The kind reaches
--jsonunchanged, so scripts branch on it. - Subject: what the change happened to, such as a container name, a listener (
:8201/tcp), a mount, or an executable. - Detail: what happened, written for a person. The wording can change between releases, so don't parse it.
The eight change kindsโ
homebutler compares containers and ports by identity, not by count. Six containers before and six after is a change when one of them is a different container.
| Kind | Means |
|---|---|
gone | A container, listener or process that was there and isn't. |
new | One that is there and wasn't. |
replaced | The same name with a different container underneath, or a process whose one invocation changed. |
image | The same container with a different image. |
state | The same container in a different state, such as running โ exited, or a process that keeps restarting. |
port | The same port answered by a different process. |
disk | A mount whose used space moved by more than 0.5 GB. |
skipped | A comparison that couldn't be made. It is never an all-clear. |
Each line below came from a real run on the test machine. gone and new name a container that disappeared and one that appeared:
gone old was running
new late now running
replaced means the container ID changed under the same name, which is what docker compose up -d does when it recreates a service. The detail gives the old and new IDs. A process is replaced only when exactly one invocation of that executable ran before and exactly one runs now, and they differ:
replaced rep recreated, d3a80c895de7 โ 6c916d09907e
replaced hbsleep same name, different invocation
image fires when the container kept its ID and the image it reports changed. In this run the imgproxy:v1 tag was removed after the container started, so Docker reported the container's image by ID. port fires when the same port is answered by a different process, here a replacement container on port 8201:
image imgproxy imgproxy:v1 โ ebfe2f904627
port :8201/tcp web (node:lts-alpine) โ web2 (node:lts-alpine)
state names the old and new state of a container that kept its ID and image:
state late running โ exited
disk shows the signed change in used space on a mount, with one decimal. This reading came from writing a 700 MB file:
disk / +0.7 GB since last report
skipped appears when a collector didn't answer, covered in When a collector doesn't answer.
When nothing changed, the terminal prints No significant changes since last report. and --json returns "notable_changes": [].
Grouped linesโ
More than five changes of one kind collapse into one line that names three subjects and counts the rest. Eight containers stopped between two reports read like this:
state 8 containers app, worker-1, worker-2, +5
Grouping applies to the terminal only. --json lists every change. A change that renders identically to another, such as a published port that Docker binds on both IPv4 and IPv6, appears once in both outputs.
What is left outโ
A report that lists everything changes every time, so these are excluded on purpose:
- Uptime and status text.
Up 4 hoursbecomesUp 9 hoursbetween any two runs. - CPU and memory readings. Both move on every sample. Memory above 85% still appears under Needs Attention.
- Restarts without a state change. A container that restarted and came back is in the state it was in.
- Processes younger than one minute. A process counts once a snapshot has seen it running for a minute. Until then it is neither
newnorgone, so something that never runs for a minute and then stops isn't reported. A saved snapshot keeps young processes, so a container recreated just before a report isn't called new the next day. - Daemons systemd starts on demand. On Linux, a service whose unit file is
static, that nothing wants or requires, and that a D-Bus name or socket starts gets noneworgoneline.fwupdandpackagekiton Ubuntu are examples.polkitdcan be too, so its stopping isn't reported on a host where nothing requires it.udisks2has a bus name, but it is enabled and wanted bygraphical.target, so it is reported.ModemManageris compared as usual on Ubuntu, where something requires it. A service that a timer or path unit starts, such asapt-dailyorfstrim, isn't on demand: one still running a minute later has hung, and it is reported asnew. When homebutler can't determine the service, the process is compared like any other. That covers macOS, hosts without systemd, a process in a user session or a container, a cgroup that can't be read, and asystemctlcall that fails or doesn't answer within 2 seconds. The snapshot marks an on-demand process withon_demand: true, because agoneis judged from the older snapshot. - Processes that belong to a container or to
watch. This covers anything running inside a container, Docker's per-containercontainerd-shimanddocker-proxy, and the helperswatch startruns. The container line already reports the container. The tradeoff is that a command line changing inside a container that kept running isn't reported. Thewatchprocess itself is compared, so it stopping is reported. - Command lines. A snapshot stores each process's name, as the kernel reports it (
pscomm), and a 12-character hash of its full command line, never the arguments, because arguments carry secrets.
A daemon that restarted with the same command line just before a report is the same daemon, and gets no line. One that restarted just before the last report and has started again since gets state <name> restarting โ under a minute old at this report and the last, which can also mean a daemon reloaded on a schedule. A crash loop that changes its command line each time is a different invocation each time, so it shows as gone once. An on-demand daemon young at two reports in a row isn't restarting, since being started again is what it does.
A process is identified by its name and that hash. python3 /opt/old.py becoming python3 /opt/new.py is a replaced. When one of several python3 processes exits, the report says gone with the detail 1 invocation, others still running, because nothing was replaced.
Needs Attentionโ
Needs Attention lists what is worth acting on now, whether or not it changed. It has three sources:
- Thresholds on the current reading: memory above 85%, a disk above 85%, and containers that are stopped.
- Ports that became reachable: a port reachable from every interface now that wasn't before, or a public port answered by a different process.
- Containers that stopped: one that was running at the last report and isn't now, and one that was recreated or took a new image and isn't running.
Each entry carries a kind in --json: memory, disk, container or port. Past five stopped containers, the entries collapse into one line: 8 containers stopped since the last report: app, worker-1, worker-2, +5. A line in the port source reads:
โ ๏ธ Port :8201/tcp is answered by web2 (node:lts-alpine) now, and was not at the last report
homebutler computes Needs Attention from the snapshots, never from the rendered change lines, so a change in a line's wording doesn't change what needs attention.
Suggested Actions repeats the same findings as a command to run, such as homebutler docker logs fresh. The doctor command applies different thresholds for disk and CPU, listed in Doctor.
When a collector doesn't answerโ
A section whose collector didn't answer is reported as skipped, not compared. If Docker is down at either snapshot, homebutler doesn't report every container as gone. The collectors are docker, ports and processes. With DOCKER_HOST pointing at a socket that doesn't exist, a report prints:
โโ Notable Changes โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
skipped containers not compared โ Docker did not answer
The status section says the same, and a warning ends the report: docker: docker daemon is not running: exit status 1. In --json, the container counts are null and the failed collector is named:
{
"running_count": null,
"stopped_count": null,
"warnings": ["docker: docker daemon is not running: exit status 1"],
"failed_collectors": ["docker"]
}
Check failed_collectors before you trust a count. A null count means nobody counted, and 0 means the collector answered and found none.
The first run, and a run after the previous snapshot became unreadable, also produce a skipped line. That line says First inspection โ baseline snapshot created. or Nothing compared โ the previous snapshot could not be read. A new baseline was created.
Snapshotsโ
Each saved run writes ~/.homebutler/reports/snapshots/snapshot_<timestamp>.json, such as snapshot_20261025T181341Z.json. After saving, homebutler deletes the oldest files until --keep remain. Pruning counts files, not days, so the span --keep covers depends on how often you run report. Snapshots are ordered by the timestamp in the file name.
A snapshot holds the system reading, the container list, the listening ports, the process identities, and which collectors failed. On the test machine, with 42 containers and 34 process identities, a snapshot was 17 KB. The largest measured case is a desktop macOS with 639 process identities, where a snapshot is about 50 KB and --keep 30 holds roughly 1.5 MB. Weigh that before raising --keep.
Use the report from a scriptโ
homebutler report --json returns every section as typed fields, with no grouping and no truncation. Branch on kind and read target for the subject:
homebutler report --json | python3 -c '
import json, sys
for c in json.load(sys.stdin)["notable_changes"]:
print(c["kind"], c["target"])
'
On the run that stopped fresh, the entries for that container were:
{
"needs_attention": [
{
"kind": "container",
"target": "fresh",
"text": "fresh stopped since the last report"
}
],
"notable_changes": [
{
"kind": "state",
"target": "fresh",
"detail": "running โ exited",
"text": "state: fresh โ running โ exited"
}
]
}
The same response carries is_baseline, snapshot_saved, compared_to (the timestamp of the snapshot it compared against), the system reading, and suggested_actions. An action with a runnable command also carries command, runner and tool:
{
"text": "Check why fresh is not running: homebutler docker logs fresh",
"command": "homebutler docker logs fresh",
"runner": "mcp",
"tool": "docker_logs"
}
An agent reads the same data from the report MCP tool. See JSON output for the full field list.