🩺 Health Check

scripts/healthcheck.py is a regression check for changes to the running system. Large changes to Octo are made as a series of small, named steps; the health check snapshots the system before a step, probes it again afterwards, and reports everything that changed without being declared. A step that breaks something is caught before the next one lands, while it is still easy to undo. It is part of the Engineering Harness.

scripts/healthcheck.py baseline --step <step>     # before the change

## ... make the change ...
scripts/healthcheck.py verify --step <step>       # after: GO or NO-GO
scripts/healthcheck.py wait-ready --step <step>   # after any Gateway restart
scripts/healthcheck.py probes                     # list the probes

verify exits 0 for GO, 1 for NO-GO, and 2 if the tool itself fails. Add --strict to treat warnings as NO-GO.

What it checks

28 probes, all read-only except the remote-access smoke test, which sends requests that are expected to be rejected:

Area Checks
Gateway /healthz, /startupz, /readyz (agent databases, channels, plugin reload, event loop), openclaw health, CLI and Gateway versions match, stability bundles and memory pressure, listening ports
Channels Each enabled channel configured, running, connected and ready; the iMessage bridge process
Hooks main can never be run by a hook; the remote-access smoke test; webhook-proxy and GitHub hook logs
Plugins Enabled plugins loaded; degraded plugins or secret owners
Agents Agent list and models, database files present and sane, session counts (if an agent database is refused, its session count drops to zero)
Cron Scheduler on; job errors, especially agent-database admission failures; overdue jobs; failed runs since the baseline
Services LaunchAgents running, plist hashes, program arguments, env key names; the Glances API; the OneDrive app (vault sync); service log errors since the baseline
Config openclaw.json hash and a redacted copy; env files as key names and value lengths only; service configs
Tailscale Funnel Tailnet up; Funnel routes, and drift from the committed config/tailscale/funnel.json
Devices Paired devices, admin count, pending requests, nodes
Logs New Gateway log lines since the baseline, matched against known failure signatures

Before and after

baseline records an inventory of the system. verify takes a new one and sorts every difference into a class:

Class Meaning Result
telemetry Values that move on their own: timestamps, process IDs, database sizes, log offsets, service runtime state ignored
expected Declared in the step’s expected-changes file pass
counter A counter that should never go up went up: cron errors, service restarts, failed deliveries, admin devices FAIL
soft A change worth a warning, such as a laptop node going to sleep WARN
undeclared Anything else that changed, appeared or disappeared FAIL

Each step declares what it is supposed to change in config/healthcheck/steps/<step>.json, including whether a Gateway or service restart is expected. Known findings that predate the step live in config/healthcheck/accepted.json.

After a restart

wait-ready polls /healthz, then /startupz, then /readyz, for up to 8 minutes by default (--timeout changes this), because an agent database can take minutes to be admitted after a restart. If an agent database is refused, it stops waiting and prints the recovery steps: stop the Gateway (openclaw gateway stop --force), back up ~/.openclaw/agents/<id>/agent, run openclaw doctor --fix --yes, then wait-ready again. Once the Gateway is ready, it runs the Gateway, channel, agent, cron, LaunchAgent and log checks.

Reports and secrets

Reports go to ~/.openclaw/healthcheck/steps/<step>/, readable only by the owner (mode 600). They never contain secret values: env files are recorded as key names and value lengths, config secrets are redacted, and log samples pass through a redactor.

Why it exists

On 2026-10-24 a routine Gateway restart left the main agent’s database refused. For about 80 minutes iMessage showed typing indicators but Octo never replied. /readyz, the Gateway log and the cron state all showed the failure, but nothing was reading them. Replaying that day’s log against a baseline makes verify fail on exactly those signals.