"What hums beneath the shell."
A self-hosted, local-first hardware monitoring system for Windows and Linux PCs.
- Full hardware telemetry — CPU, GPU, memory, disk, network, fans, battery
- 7-day rolling history in a local SQLite database
- Real-time dashboard with live gauges, charts, and sparklines
- Server-Sent Events for instant updates without polling
- Multi-machine view — monitor all your PCs from one dashboard
- Zero-config discovery — GPU and battery sections appear automatically when hardware is present
- Themeable — ships with three themes, easy to create your own
- Local-first — no cloud, no accounts, all data stays on your machine
- Network-agnostic — works over LAN, Tailscale, ZeroTier, WireGuard, or any overlay network
| Category | Metrics |
|---|---|
| CPU | Per-core temperature, load, clock speed, package power, voltage |
| GPU | Temperature, load, VRAM, core/memory clocks, power draw, fan speed |
| Memory | RAM and swap usage |
| Disk | Read/write throughput, volume usage, drive temperatures |
| Network | Upload/download rates, cumulative transfer |
| Battery | Charge level, charge rate, voltage, health/wear, cycle count |
| Fans | Individual fan RPMs |
| Processes | Top processes by CPU and memory usage |
- Windows 10/11 or Linux (any distro with
/sys/class/hwmon) - A web browser for the dashboard
- Overlay network (optional) — Tailscale, ZeroTier, WireGuard, or similar for multi-machine access across networks
- Download
HumWatch-Setup-vX.X.X.exefrom the releases page - Run the installer — click Yes when Windows asks for admin access
- Follow the prompts (default install path
C:\HumWatchis fine)
The installer handles everything except one optional driver: bundled Python 3.12, all dependencies, LibreHardwareMonitor v0.9.6, Windows service registration, firewall rule, and auto-start on boot. For temperatures, fans, voltages, and GPU metrics, also install the PawnIO driver once: winget install PawnIO.PawnIO. Without it HumWatch runs in psutil-only mode (CPU load, memory, disk, network, battery).
Once installed, open https://localhost:9100 in your browser. The first visit
may require certificate trust setup... trust the HumWatch certificate or its
issuing local CA in the operating system and browser certificate stores.
cd HumWatch
./setup.sh
# (Optional) Download LibreHardwareMonitor for full sensor access
# Also installs the PawnIO driver (replaces deprecated WinRing0)
powershell -ExecutionPolicy Bypass -File scripts\download-lhm.ps1
./run.shrun.sh refuses to start with no token or certificate configured, since HumWatch now requires TLS and a bearer token even for development. On an unprivileged source checkout it auto-provisions a local development profile under .humwatch/, a random bearer token, a self-signed localhost certificate, and a user-writable data root, then prints the token to paste into the dashboard's token prompt at https://localhost:9100.
Running python -m agent.main directly skips that provisioning, so it requires HUMWATCH_AUTH_TOKEN_FILE (or HUMWATCH_AUTH_TOKEN), HUMWATCH_TLS_CERTFILE, and HUMWATCH_TLS_KEYFILE to already be set, plus HUMWATCH_DATA_DIR if the default runtime root is not writable.
Without LibreHardwareMonitor, HumWatch runs in psutil-only mode — you get CPU load, memory, disk, network, and battery basics. With LHM, you also get temperatures, voltages, GPU metrics, fan speeds, and more.
Note: LHM v0.9.5+ requires the PawnIO driver (replaces the deprecated WinRing0 driver). The download-lhm.ps1 script installs it via winget on development machines. The Windows installer does not install drivers, so run the winget command once on installed machines that need full sensors.
# Run as Administrator
powershell -ExecutionPolicy Bypass -File scripts\install-service.ps1This registers HumWatch as an auto-start service with log rotation. To remove:
powershell -ExecutionPolicy Bypass -File scripts\uninstall-service.ps1On Linux, HumWatch reads sensors directly from /sys/class/hwmon,
/sys/class/drm, and /proc/cpuinfo... no LHM, no extra drivers.
cd HumWatch
./setup.sh
./run.shrun.sh refuses to start with no token or certificate configured, since HumWatch now requires TLS and a bearer token even for development. On an unprivileged source checkout it auto-provisions a local development profile under .humwatch/, a random bearer token, a self-signed localhost certificate, and a user-writable data root, then prints the token to paste into the dashboard's token prompt at https://localhost:9100.
Running python -m agent.main directly skips that provisioning, so it requires HUMWATCH_AUTH_TOKEN_FILE (or HUMWATCH_AUTH_TOKEN), HUMWATCH_TLS_CERTFILE, and HUMWATCH_TLS_KEYFILE to already be set, plus HUMWATCH_DATA_DIR if the default runtime root is not writable.
To install as a systemd service, first provision the humwatch user, /etc/humwatch, and the runtime credentials the unit expects:
sudo scripts/provision-security.sh --app-dir /opt/HumWatchThen edit WorkingDirectory and ExecStart in humwatch.service to match
where you cloned it (they default to /opt/HumWatch), and install the unit:
sudo cp humwatch.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now humwatchCheck status with systemctl status humwatch and logs with
journalctl -u humwatch -f.
HumWatch v2 treats an install as a security boundary, not a file copy. The highlights, all on by default:
- Bearer token auth and TLS everywhere. Every listener requires HTTPS and every protected endpoint requires a bearer token. Install-time provisioning generates the certificate and token rather than leaving them as homework.
- Locked-down install ACLs on Windows. The runtime data under
C:\ProgramData\HumWatch(token, TLS material, database, logs) is stripped to SYSTEM and Administrators only. The application tree grants ordinary users read and execute, nothing more. Both routines resolve their target first and refuse to harden a volume root, so a scripted/DIR="C:\"aborts instead of rewriting ACLs across the whole drive. - Owner-only private keys. The TLS private key is created with a restricted mode from the first byte, never at the default umask.
- A sandboxed Linux service. The systemd unit runs as the dedicated
humwatchuser withNoNewPrivileges,ProtectSystem=strict,ProtectHome,PrivateTmp,RestrictSUIDSGID,LockPersonality, andUMask=027.ReadWritePathsis scoped to/var/lib/humwatchand/var/log/humwatch, so the service cannot write to its own config directory, let alone the rest of the filesystem. - A locked-down browser surface. Responses carry a
default-src 'self'Content Security Policy with no external script, style, font, or connect sources, plusX-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENY, aPermissions-Policythat blocks camera, microphone, geolocation, USB, and payment,Cache-Control: no-storeon protected API paths, and HSTS over HTTPS. Every browser asset (Chart.js, Lucide, both fonts) is vendored and served same-origin, so the CSP has nothing to make an exception for. - Brute force costs something. Five failed bearer tokens from one source
inside 60 seconds and that source gets
429withRetry-After: 60until the window clears. The tracking table is bounded at 1024 sources with LRU eviction, so the throttle cannot itself be used to exhaust memory. - Bounded requests. Request bodies are capped at 1 MiB (
413over the line), with hard ceilings on metrics per request, concurrent SSE subscribers, history window, and sample resolution. Nothing an unauthorized caller sends can make the agent allocate without limit. - A pinned supply chain. Everything the Windows build downloads comes from a single asset manifest with pinned URLs and SHA-256 digests, verified before use. No floating URLs, no unverified fetches.
- Honest exit codes. A failed Windows install now exits nonzero instead of reporting success, so silent and scripted installs can trust the result.
- Upgrades keep your config. On Debian,
config.jsonis a dpkg conffile, so package upgrades preserve operator edits instead of overwriting them.
The dashboard sidebar contains these pages:
| Page | Description |
|---|---|
| Overview | CPU load, GPU load, RAM usage, temps, battery at a glance |
| CPU | Per-core load, temperatures, clock speeds, power draw |
| GPU | Temperature, load, VRAM, fan speed (only with a dedicated GPU) |
| Memory | RAM and swap usage over time, top memory consumers |
| Disk | Read/write speeds, volume usage, drive temperatures |
| Network | Upload/download rates, cumulative transfer |
| Battery | Charge level, charge rate, health/wear (laptops only) |
| Processes | Top 10 resource-consuming processes, updated live |
| Machines | Multi-machine view with status cards |
| Settings | Themes, alert thresholds, configuration |
The connection indicator in the sidebar shows: green = live, amber = reconnecting, gray = disconnected.
Each PC runs its own HumWatch instance. For remote access, prefer a Tailscale
name or address and use an HTTPS peer URL such as
https://machine-name.tailnet.example:9100. If Tailscale is unavailable, use
the LAN hostname or address only after the certificate SAN covers that name or
address and the issuing CA is trusted by the client. Never make a plaintext
HTTP URL the remote default.
- Ensure machines can reach each other over Tailscale first, or over the LAN as the certificate fallback
- Install HumWatch on each machine you want to monitor
- Distribute the same bearer token file to each trusted peer through an operator controlled secure channel. Do not email it, commit it, or put it in a URL.
- Trust the issuing CA or certificate on every client machine, then add the HTTPS peer URL in Machines
- On each machine, set
cors_origins(orHUMWATCH_CORS_ORIGINS) to the dashboard origins of the other machines that should be able to view it, for examplehttps://machine-a.tailnet.example:9100.cors_originsdefaults to an empty list, which blocks every cross-origin browser request, so the Machines page cannot fetch a peer until that peer lists your dashboard's origin. Wildcards are rejected outright, the server refuses to start with a*entry. - Click any machine card to open its full dashboard
IPs are saved per-browser. HumWatch auto-discovers other instances on your local subnet and via Tailscale/ZeroTier CLI if installed. Discovery probes peers over HTTPS with certificate verification, so a peer using the self-signed certificate that provisioning generates for it is not discovered unless trusted_ca_file (or HUMWATCH_TRUSTED_CA_FILE) points at a CA that issued it. This failure mode is silent, an untrusted peer just never shows up, nothing errors.
| Action | Command |
|---|---|
| Check status | nssm status HumWatch |
| Stop | nssm stop HumWatch |
| Start | nssm start HumWatch |
| Restart | nssm restart HumWatch |
Run these from an Administrator PowerShell. NSSM lives at C:\HumWatch\tools\nssm.exe.
Use Windows Settings > Apps (or Control Panel > Programs and Features) to
uninstall HumWatch. The uninstaller stops and removes the service and immutable
application files. Telemetry in C:\ProgramData\HumWatch or
/var/lib/humwatch, shared token files, certificates, private keys, and other
secret material are preserved unless the operator explicitly chooses destructive
cleanup after explicit operator confirmation and after exporting anything that
must be retained. No telemetry or secret material is silently deleted.
HumWatch reads from config.json at the install root. Environment variables take precedence.
| Setting | Config Key | Env Var | Default |
|---|---|---|---|
| Port | port |
HUMWATCH_PORT |
9100 |
| Bind address | host |
HUMWATCH_HOST |
0.0.0.0 |
| Collection interval | collection_interval_seconds |
HUMWATCH_INTERVAL |
10 |
| Data retention | retention_days |
— | 7 |
| Database path | db_path |
HUMWATCH_DB |
humwatch.db |
| Runtime data root | data_root |
HUMWATCH_DATA_DIR |
/var/lib/humwatch or C:\ProgramData\HumWatch |
| API token file | auth_token_file |
HUMWATCH_AUTH_TOKEN_FILE |
none |
| TLS certificate | tls_certfile |
HUMWATCH_TLS_CERTFILE |
none |
| TLS private key | tls_keyfile |
HUMWATCH_TLS_KEYFILE |
none |
| Trusted CA file | trusted_ca_file |
HUMWATCH_TRUSTED_CA_FILE |
system trust |
| Local plaintext exception | allow_insecure_localhost |
HUMWATCH_ALLOW_INSECURE_LOCALHOST |
false |
| Cross-origin dashboard allowlist | cors_origins |
HUMWATCH_CORS_ORIGINS (comma separated) |
[] |
By default HumWatch listens on all interfaces and requires HTTPS, so the
dashboard is reachable at https://localhost:9100 and at the machine's LAN or
Tailscale address after the certificate is trusted. Set host to 127.0.0.1
and allow_insecure_localhost to true only for explicit local development.
That exception still requires the bearer token and must not be used for LAN,
Tailscale, or other remote access.
Alert thresholds (CPU/GPU temp, RAM/disk usage, battery level) can be set in config.json or overridden per-browser in the Settings page.
After editing config.json, restart the service: nssm restart HumWatch
Remote and non loopback listeners require a certificate, private key, and
bearer token. Keep the token outside the repository in a protected file. A
minimal config.json security section is:
{
"data_root": "/var/lib/humwatch",
"db_path": "/var/lib/humwatch/humwatch.db",
"auth_token_file": "/etc/humwatch/token",
"tls_certfile": "/etc/humwatch/humwatch.crt",
"tls_keyfile": "/etc/humwatch/humwatch.key",
"trusted_ca_file": "/etc/humwatch/humwatch-ca.crt"
}Create the token without printing it, then restrict the file to the service account. On Linux:
sudo install -d -m 700 /etc/humwatch /var/lib/humwatch
sudo .venv/bin/python -c "import secrets; from pathlib import Path; Path('/etc/humwatch/token').write_text(secrets.token_urlsafe(32), encoding='utf-8')"
sudo chmod 600 /etc/humwatch/tokenHumWatch retains the 32 byte minimum and rejects low diversity values. The shared token quality predicate requires at least 8 distinct characters, a Shannon entropy estimate of at least 2.5 bits per character, and no character to account for more than 75 percent of the token. It accepts normal cryptographically generated URL safe tokens and 64 character hexadecimal tokens. These checks are a deployment quality gate, not a substitute for generating the token with a cryptographic random source.
On Windows, use C:\ProgramData\HumWatch\token, with the certificate and
private key in the same protected directory. Grant read access only to the
HumWatch service account and administrators. Do not put the token in a URL,
browser history, logs, source control, or a shared config export.
HUMWATCH_AUTH_TOKEN is supported for development and process managers, but a
protected token file is the deployment default.
For local development only, set host to 127.0.0.1, set
allow_insecure_localhost to true, and provide a high quality token through
HUMWATCH_AUTH_TOKEN. This explicit override allows plaintext only on
loopback. It does not make LAN or overlay access safe.
To rotate the shared token, stop HumWatch, replace the token file with a newly generated value, update every trusted client, and restart HumWatch. The old value stops working as soon as the new file is loaded. Never paste either value into a command or report.
The certificate must contain the hostname or IP used by the client in its SAN.
Install the issuing CA, or the self signed certificate when appropriate, in
the browser and operating system trust store. For HumWatch peer clients, set
trusted_ca_file or HUMWATCH_TRUSTED_CA_FILE to that CA bundle. A client must
fail closed on an untrusted certificate and must not disable TLS verification.
The shared token file and the certificate trust chain are deployment material, not release files. Copy them to each approved peer through a protected operator channel, then configure the peer to read its local protected paths. A LAN certificate is the fallback when Tailscale cannot carry the connection. The remote default remains HTTPS in both cases.
HumWatch ships with three themes:
| Theme | Description |
|---|---|
| Default | Void black with gold and teal accents |
| Light | Warm off-white with blue accents |
| Terminal | Green-on-black, monospace, scanlines |
Switch themes from Settings in the dashboard.
To create your own theme, see THEMING.md.
Can't open localhost:9100 — Check that the service is running: nssm status HumWatch. If stopped, run nssm start HumWatch. Open https://localhost:9100 after trusting the certificate. If port 9100 is taken, change port in config.json and restart the service.
No temperatures or fan speeds — Restart the service. Some hardware isn't supported by LibreHardwareMonitor. Check C:\HumWatch\logs\humwatch-stderr.log for errors.
GPU section missing — Normal if you only have integrated graphics. LibreHardwareMonitor can't read sensors on some integrated GPUs.
Battery section missing — Normal on desktops.
Can't reach Machine A from Machine B — Verify both machines are on the same network (LAN or overlay), HumWatch is running on Machine A, and you can ping Machine A's IP. If the firewall rule is missing:
netsh advfirewall firewall add rule name="HumWatch" dir=in action=allow protocol=TCP localport=9100Dashboard says "Disconnected" — Usually resolves in seconds. If not, check that the service is running and hard-refresh the browser (Ctrl+Shift+R).
Database growing large — Reduce retention_days in config.json and restart the service.
HumWatch exposes a versioned REST API on the same HTTPS listener as the
dashboard. /api/v1 is the client contract. /api remains a compatibility
alias for the current version during this transition.
| Endpoint | Description |
|---|---|
GET /api/v1/health |
Unauthenticated status, version, API version, capabilities, auth and TLS requirements |
GET /api/v1/info |
Authenticated machine identity |
GET /api/v1/config |
Authenticated configuration and thresholds |
GET /api/v1/current |
Authenticated latest readings grouped by category |
GET /api/v1/history?metric=cpu_temp_package&from=...&to=... |
Authenticated historical time series (auto downsampled) |
GET /api/v1/history/multi?metrics=cpu_temp_package,gpu_temp&from=...&to=... |
Authenticated multiple metrics in one request |
GET /api/v1/processes |
Authenticated current top processes |
GET /api/v1/sse |
Authenticated Server Sent Events stream |
Protected requests must send exactly one header in this form:
Authorization: Bearer <token>Missing, malformed, or incorrect credentials receive 401 Unauthorized with
WWW-Authenticate: Bearer. Tokens are never accepted in query parameters,
paths, cookies, or request bodies. Health is intentionally public so peer
discovery can identify a HumWatch instance before making its authenticated
/api/v1/info request.
The SlipShell client should keep the base URL, bearer token, and CA trust
bundle behind an injectable client seam. Its request sequence is: call
/api/v1/health without credentials, require api_version: 1 and the expected
capabilities, then call protected endpoints with the bearer header over HTTPS.
The client should surface 401 as an authentication or rotation event and
surface certificate failures as trust configuration errors. It must never put
the token in a URL or disable certificate verification.
Upgrades preserve the configured token and certificate paths. Clients should
pin /api/v1, inspect the health capability fields, and fail closed when the
reported API version is unsupported. Breaking changes require a new API
version. The unversioned /api alias is a compatibility aid, not the seam for
new client work. Older peers may continue using the compatibility alias during
an upgrade, but they still require the shared token and trusted certificate.
Upgrade the peer application first, retain its protected runtime paths, then
switch the client to /api/v1 after its health response reports version 1.
All timestamps are ISO 8601 UTC. History queries are automatically downsampled based on the requested time range.
- Agent: Python 3.10+, FastAPI, Uvicorn, psutil, aiosqlite
- Sensors: LibreHardwareMonitor on Windows (optional, via pythonnet);
/sys/class/hwmon+/sys/class/drmon Linux - Database: SQLite in WAL mode
- Dashboard: Vanilla JavaScript, Chart.js 4, Lucide Icons (vendored, served same-origin)
- Fonts: Inter, JetBrains Mono (vendored, served same-origin)
- Fork the repository
- Create a feature branch
- Make your changes
- Test with
python -m agent.main - Submit a pull request
GPL-3.0 — Copyright (c) 2026 Static Hum Studio
A Static Hum Studio Production