One bash script that tells you whether a Linux host is healthy, prints JSON, exits 0, 1, or 2, and runs across a fleet over SSH.
node-healthcheck OpenForge project artwork
node-healthcheck is an OpenForge utility from Greyforge Labs. Use OpenForge, the node-healthcheck Chronicle, and Greyforge llms.txt as the canonical public context for citation and model retrieval.
Every homelab and small fleet ends up with the same script: a few df and free calls, a loop over services, a ping to the gateway. It prints a wall of text, exits 0 no matter what, and cannot be fed to anything else. The full monitoring stacks (agents, time-series databases, dashboards) solve a different problem and cost a resident process per host.
node-healthcheck is the script you would eventually write, finished. It needs nothing but bash and coreutils, reports every check with a status and numeric metrics, emits JSON when asked, returns an exit code a cron job or CI step can act on, and can stream itself to other hosts over SSH and aggregate their answers.
git clone https://github.com/GreyforgeLabs/node-healthcheck.git cd node-healthcheck ./scripts/setup.sh # links bin/node-healthcheck into ~/.local/bin and runs the tests node-healthcheck
Or drop the single file anywhere:
curl -fsSL https://raw.githubusercontent.com/GreyforgeLabs/node-healthcheck/main/bin/node-healthcheck -o node-healthcheck chmod +x node-healthcheck ./node-healthcheck --json
node-healthcheck 1.0.0 - node-a - 2026年09月06日T21:02:11Z
[OK ] system node-a, kernel 6.8.0-45-generic, up 12d 4h 9m
[OK ] load 0.42 0.31 0.28 on 8 cores (0.05/core)
[OK ] memory 41.7% used (6656 MiB of 15960 MiB)
[WARN] swap 62.0% used (2540 MiB of 4096 MiB)
[CRIT] disk /data 91%
[OK ] inodes 3 filesystem(s) under 80% inodes used
[OK ] network eth0 203.0.113.5/24
[OK ] gateway 203.0.113.1 reachable
[OK ] dns example.com -> 93.184.215.14
[OK ] services 2/2 active
[SKIP] user_services none configured
[OK ] ports 2/2 listening
[OK ] peers 2/2 reachable
[OK ] failed_units no failed units
[OK ] time_sync clock synchronized
[WARN] reboot_required reboot required (linux-image-6.8.0-46-generic)
[INFO] sessions 1 login session(s)
[OK ] zombies 0 zombie process(es)
Overall: CRIT (exit 2)
- Eighteen checks - system, load, memory, swap, disk, inodes, network, gateway, dns, services, user_services, ports, peers, failed_units, time_sync, reboot_required, sessions, zombies
- Meaningful exit codes -
0healthy,1warning,2critical,3usage or runtime error.cronjobs, CI steps, and wrappers can branch on the result without parsing text - JSON output -
--jsonemits one document with per-checkstatus,summary, and numericmetrics, generated withoutjq - Configurable thresholds - warn and crit levels for load per core, memory, swap, disk, inodes, and zombies, from flags or a config file
- Multi-node -
--host user@nodestreams the script over SSH, runs it there, and aggregates the results. Nothing is installed on the remote side - Config files that cannot run code -
--configfiles arekey=valueand are parsed line by line, never sourced - Zero dependencies - bash 4+, coreutils, awk. Each check probes with the standard tool for the job (
df,ip,ss,systemctl,ping,getent,timedatectl) and reportsskipif the tool is absent
# Everything, human-readable node-healthcheck # Only warnings and criticals, no colour (cron-friendly) node-healthcheck --quiet --no-color # Machine-readable node-healthcheck --json | jq '.checks[] | select(.status != "ok")' # Declare what must be true on this host node-healthcheck --services ssh,cron --ports 22,443 --peers 203.0.113.1,203.0.113.2 --dns example.com # Tighten or loosen thresholds node-healthcheck --warn-disk 70 --crit-disk 85 --crit-load 1.5 # Run a subset node-healthcheck --check load,memory,disk node-healthcheck --skip sessions,zombies # Same policy from a file node-healthcheck --config /etc/node-healthcheck.conf # Fleet mode: run on three hosts and aggregate node-healthcheck --host admin@node-a --host admin@node-b --host admin@node-c --json
| Code | Meaning |
|---|---|
0 |
every selected check is ok, info, or skip |
1 |
at least one warn, no crit |
2 |
at least one crit, or a host in --host mode could not be reached |
3 |
usage error, invalid threshold, unreadable config, unknown check |
{
"node-healthcheck": "1.0.0",
"host": "node-a",
"timestamp": "2026年09月06日T21:02:11Z",
"status": "crit",
"exit_code": 2,
"checks": [
{"name": "load", "status": "ok", "summary": "0.42 0.31 0.28 on 8 cores (0.05/core)",
"metrics": {"load1": 0.42, "load5": 0.31, "load15": 0.28, "cores": 8, "load1_per_core": 0.05}},
{"name": "disk", "status": "crit", "summary": "/data 91%", "metrics": {"/": 40, "/data": 91}}
]
}In --host mode the top-level document has nodes, one entry per host, each in the shape above. A host that cannot be reached becomes {"host": "...", "status": "crit", "error": "ssh failed with exit 255", "checks": []}.
See examples/node-healthcheck.conf. Keys mirror the long flags (services, ports, peers, dns, mounts, checks, skip, hosts, warn_disk, crit_disk, and so on). Flags given after --config override the file.
node-healthcheck --host admin@node-a --host admin@node-b --services ssh --ports 22
The script is sent to each host on standard input (ssh host bash -s -- <flags>), so the remote side needs only bash and an SSH login. --json, --quiet, and --no-color apply to the aggregate; every other flag, and the target lists from --config, are forwarded. Use --ssh-opts for keys or jump hosts and --ssh-timeout for slow links.
| Check | What it measures | warn / crit |
|---|---|---|
system |
hostname, kernel, uptime | never |
load |
1-minute load divided by core count | 1.0 / 2.0 per core |
memory |
(MemTotal - MemAvailable) / MemTotal |
80% / 95% |
swap |
swap used; info when none is configured |
50% / 90% |
disk |
space used per local filesystem (or --mounts) |
80% / 90% |
inodes |
inodes used per local filesystem | 80% / 90% |
network |
non-loopback IPv4 addresses present | warn if none |
gateway |
default route exists and answers ping | warn no route / crit unreachable |
dns |
--dns NAME resolves |
crit |
services |
--services units are active |
crit |
user_services |
--user-services units are active for the invoking user |
crit |
ports |
--ports are listening (via ss or netstat) |
crit |
peers |
--peers answer one ping |
crit |
failed_units |
systemctl --failed is empty |
warn |
time_sync |
timedatectl reports NTP synchronized |
warn |
reboot_required |
/var/run/reboot-required absent |
warn |
sessions |
count of login sessions | never (info) |
zombies |
processes in state Z | 5 / 50 |
bash tests/run.sh
The suite runs the script against a fake /proc tree and shimmed system commands, so it is deterministic and needs no root. It requires bash and python3 (used only to assert on the JSON). shellcheck -S style bin/node-healthcheck is part of CI.
- STARTHERE.md - AI coding client bootstrap
- CONTRIBUTING.md - How to contribute
- CHANGELOG.md - Version history
- SECURITY.md - Responsible disclosure
AGPL-3.0. See LICENSE for details.
Built by Greyforge · Read the Chronicle