Monitor a Linux server or laptop from your phone, with nothing running in the middle.
StatixAgent is a single static Go binary that watches one machine and talks to your own private Telegram bot. It samples the system, watches sshd, and pushes alerts straight to your chat. There is no central server, no dashboard to host, and no third party in your data path — the agent talks only to Telegram's Bot API and to GitHub Releases when you ask it to update.
Everything after installation happens in Telegram: type /status for a live
dashboard, get a push message the moment someone logs in over SSH or the power
cord is pulled, and tap buttons to drill in.
curl -fsSL https://raw.githubusercontent.com/eliau2005/statixagent/main/install.sh | sudo bash
Project status: early open-source release (v0.x). The core is built, tested, and running; the project is now opened to the community. See Project status and ROADMAP.md.
Most monitoring stacks assume you are running a fleet and want a time-series database, a query language, and a dashboard. For a single VPS or a home laptop, that is a lot of moving parts to install, secure, and keep alive — often heavier than the thing you are trying to watch.
StatixAgent takes the opposite trade-off:
| Prometheus / Grafana-style | StatixAgent | |
|---|---|---|
| Components to run | exporter + TSDB + dashboard + alertmanager | one binary |
| Where your data goes | scraped into a central store | stays on the machine; only alerts you request leave |
| How you interact | a web UI you host and secure | a private Telegram bot you already carry |
| Footprint | designed for fleets | designed for one box (MemoryMax=128M) |
| Setup | compose files, config, reverse proxy | one command, a one-time TUI wizard |
It is deliberately not a fleet tool. Each install is fully isolated: one agent, one bot, one chat. That buys simplicity and a small attack surface, at the cost of cross-server correlation (see Trade-offs).
Reach for StatixAgent when you want to know your one box is okay — and hear about it the moment it is not — without standing up an observability stack.
- Core system — CPU (total, per-core, load average), RAM and swap, disk space per mount, disk I/O rates, network rates and lifetime totals per interface, uptime, process count, open file descriptors. CPU and memory alerts only fire once usage stays over the threshold for about a minute, so a cron job or backup burst stays quiet.
- Thermal — temperatures (thermal zones + hwmon), fan speeds, and throttling detection. Temperature alerts only fire once the threshold is held for several consecutive samples, so a CPU touching 90 °C for a moment mid-boost stays quiet.
- Battery & power (laptops / physical machines) — charge, health vs. design capacity, wattage draw, estimated time-to-empty, and an immediate alert when the power cord is pulled.
- Workloads — systemd units, named processes, TCP ports, HTTP healthchecks, and Docker containers (status, per-container CPU/RAM, and restart-loop detection).
- Reachability — SSL/TLS certificate expiry for hosts you name, and TCP
connect latency from the server's own vantage point (
/ping).
SSH visibility is a first-class feature, not an afterthought:
- Real-time alert on every SSH login, with geo-IP of the source; successful root logins are escalated.
- Live session listing (user, source IP, duration, TTY).
- Login and failure history, kept in memory.
- Brute-force detection — a sliding-window counter per source IP with a threshold alert and quiet-period dedupe during an ongoing attack.
authorized_keyschange alerts — the agent hashes root's and every home user's key files and alerts when one changes.
If you log in often from a known address, trust it to stop the per-login alert for it. Entries can be single IPv4/IPv6 addresses or CIDR ranges, and come from two places that are merged:
-
The bot —
/trust <ip-or-cidr>,/untrust <ip-or-cidr>and/trusted(list), or the 🤝 Trust this IP button on a plainSSH loginalert. The button opens a confirm screen restating the address; only ✅ Yes, trust acts, and ❌ Cancel puts the alert's buttons back. Changes apply to the next login, with no restart, and are kept in/etc/statix-agent/trusted_ips(0600, one entry per line). Up to 64 entries;/untrustremoves only entries added this way. -
The config — a static baseline, changed by editing the file and restarting (
systemctl restart statix-agent):[ssh] trusted_ips = ["203.0.113.7", "198.51.100.0/24", "2001:db8:42::/48"]
/trustedlists these separately, and/untrustsays to edit the file instead. Re-running the installer keeps the existing[ssh]settings.
Both lists are empty by default, which keeps the alert-on-every-login
behavior. Both use the same rules: invalid entries are rejected (at startup
for the config, in the reply for /trust), and so are ranges that would trust
everything (0.0.0.0/0, ::/0) and IPv4-mapped ranges shorter than /96.
/trust also refuses an address already covered by an existing entry and
names it. The trust button only appears on plain logins from an IP address
that is not trusted yet — never on root, new-key or brute-force alerts.
A suppressed login still appears in /ssh history and the daily digest; only
the push is skipped. Some logins from a trusted address still alert:
- root logins always raise the
ROOT SSH loginalert; - a key login with a key that user has never logged in with before raises a
SSH login with new keyalert (once per user and key; see below); - a login from an address that failed to authenticate within the last two minutes alerts as usual, since a success right after failures is worth a look;
- failed attempts are never exempt, so a burst of failures from a trusted address still raises the brute-force alert.
To recognise new keys, the agent remembers the user and SHA256 fingerprint of
key logins in /etc/statix-agent/known_keys (0600, one user fingerprint
per line). The file is only used once some source is trusted (config or bot). Keys from logins
that alert anyway (untrusted sources, root) are learned straight away; a new
key from a trusted source is learned only once its alert has been delivered,
so a snoozed or undelivered alert fires again on the next login. SSH history is
not kept across restarts, so there is nothing to learn from on the first
start: after upgrading, or after trusting the first address from the bot, the
first key login per user and key from a trusted address alerts once, and
later ones stay quiet. Deleting the file resets this.
Unreadable lines are skipped with a log message and kept in the file.
Password and keyboard-interactive logins carry no fingerprint and stay
suppressed from trusted addresses.
The source must be logged as an IP address: if sshd logs hostnames
(UseDNS yes), the trusted list cannot match (and no trust button is offered)
and the agent logs why.
Alerts arrive as push messages — you never have to ask for them:
- Threshold crossings (CPU, RAM, disk, temperature, low battery).
- Power loss, host reboot, new SSH login, brute-force burst, key-file change, certificate expiry, and container down / restart-loop events.
- Watched units, ports, HTTP checks and processes, checked every minute: an alert when one starts failing and a ✅ when it recovers, never while it stays as it was. A failure already there when the agent starts is not repeated. The alert's 🔍 Open shows the unit or list; with service actions on, a unit alert also offers 🔁 Restart, which asks for confirmation first.
Each alert carries one-tap action buttons for the obvious next step (jump to the relevant metric view, list SSH sessions, open the firewall control) and a 🔕 button to snooze that alert for an hour while you handle it.
Remediation is intentionally minimal and always gated behind an explicit confirmation:
-
SSH session disconnect buttons in
/ssh. -
/firewall— open SSH port 22 viaufw, behind a mandatory confirmation screen. Hosts withoutufwget a plain "cannot manage the firewall here" and no buttons, never a guess about what is open. Every change runs as a trial: unless you tap Keep within 2 minutes, the previous rules are restored byte for byte, even if the agent crashes or the host reboots (asystemd-runtimer, an in-process timer, and a check at startup). Closing port 22 is off by default; to allow it, set it in the config file (never from Telegram). Keep then also needs a new SSH connection seen after the change:[firewall] allow_close_ssh = true trial_seconds = 120 # 60-600
-
🤝 Trust this IP on plain SSH login alerts, behind a confirm screen (see Trusted IPs).
-
Service actions, off by default: start, stop and restart a systemd unit or a Docker container, and read its recent logs, from its screen under
/services. Turn them on in the config:[actions] enabled = true allow = ["nginx", "postgresql", "web"] # units and container names
With no
allowlist, only the watched units can be acted on, and no container. Units are never enabled or disabled.statix-agent.service,dbus*,systemd-*,*.targetand the agent's own container are always refused, and stopping or restartingssh,docker,containerdor a network manager asks twice. Logs are redacted of the bot token, but they are copied into the chat's history. Re-running the installer drops[actions], which turns them off again.
/status on a laptop that has been up six days, and a power-loss alert from the
moment its mains supply dropped.
/status |
Power lost |
|---|---|
![]() |
![]() |
Commands are registered for autocomplete, and every metric view carries an inline keyboard so you can navigate by tapping instead of typing.
| Command | Result |
|---|---|
/status |
full snapshot: CPU, RAM, disks, net, temps, battery, uptime |
/cpu /mem /disk /net /temp /battery |
one metric in detail, with sparkline trends on /cpu and /mem |
/top |
heaviest processes by CPU and memory |
/digest |
the day's recap — also auto-sent every morning |
/services |
systemd units, processes, ports, healthchecks |
/http |
endpoint checks (redirects are reported, not followed) · /http add <url> [status] · /http remove <url> |
/ping <host[:port]> |
TCP connect latency from the server |
/docker |
containers with CPU/RAM and restart counts |
/ssh |
live sessions with disconnect buttons · /ssh history · /ssh fails |
/ssl |
certificate expiry · /ssl add <host> · /ssl remove <host> |
/firewall |
open/close SSH port 22 via ufw (says it cannot manage the firewall where ufw is absent) |
/trusted |
trusted SSH sources, by origin (config / Telegram) · /trust <ip-or-cidr> · /untrust <ip-or-cidr> |
/watching |
everything watched, managed with buttons |
/settings |
alert thresholds and digest schedule, tuned with buttons |
/services_scan /ports_scan |
discover candidates to watch |
/update |
check for a new release · /update_confirm asks, then installs it |
/clear_chat |
delete recent messages (asks first) |
/help |
grouped command reference |
The Live button animates the latest regular sampler snapshot. It does not take extra samples, evaluate alert thresholds, or add points to metric trends, so opening the view cannot change monitoring behavior.
Commands are accepted only from the single chat ID in the config; updates from any other chat are dropped before parsing.
The agent can update itself from GitHub Releases, and the verification is not optional:
- Fetch the release's
checksums.txtand its detachedchecksums.txt.sig. - Verify the signature against an ed25519 public key compiled into the binary at release time. If the build carries no key, or the signature does not verify, the update aborts — there is no "skip verification" path.
- Verify the downloaded binary's SHA256 against the signed checksum.
- Swap atomically (
rename), keeping the previous binary as.prev. - A crash-loop guard at startup restores
.prevautomatically if the new binary keeps dying, and consumes it so versions cannot ping-pong.
Automatic update checks are off by default and opt-in at install time. Full details are in SECURITY.md.
-
The bot token lives in
/etc/statix-agent/config.toml, written atomically with0600permissions. -
Commands are accepted only from the configured chat ID; state-changing actions (
/update_confirm,/clear_chat,/firewall, service actions) require an explicit confirmation. The command menu is shown in that chat only. Buttons can only run what the bot's own keyboards emit; any other callback data is refused. In a private chat the sender must be the chat's own user. Forwarded messages, messages sent via an inline bot and messages sent on behalf of a chat (anonymous group admins, channels) are ignored. -
If
chat_idis a group, list who may change state:[telegram] admin_user_ids = [123456789]
Every member can then use views (status, metrics, navigation buttons), but only these users can run other commands or press other buttons. Without it every member can still do everything, and the bot posts a warning once per start; a future release will require it for groups. Private chats ignore it. Re-running the installer rewrites
[telegram], so add it again after a re-install. -
The systemd unit ships hardened:
NoNewPrivileges,ProtectSystem=full,ProtectHome=read-only,PrivateTmp, a narrowReadWritePaths, andMemoryMax=128M. -
Updates are verified with ed25519 and SHA256 before an atomic swap, with automatic rollback.
See SECURITY.md for the full threat model, the privilege model, and the distinction between what is implemented today and future hardening.
curl -fsSL https://raw.githubusercontent.com/eliau2005/statixagent/main/install.sh | sudo bashThe one-time TUI wizard asks for your bot token (from @BotFather) and chat ID, lets you toggle monitor categories and thresholds, then:
- writes
/etc/statix-agent/config.toml(0600), - installs
/usr/local/bin/statix-agent, - and enables the hardened systemd service (
Restart=always).
The TUI runs only at install time. Everything afterward is done through the
bot. Piping a script to sudo bash runs it as root — you are encouraged to read
install.sh first; it is short.
The agent runs on Linux and is released for:
linux/amd64linux/arm64
It reads /proc, /sys, the auth log (journalctl / /var/log/auth.log),
utmp (with a loginctl fallback for utmp-less distros such as Ubuntu 24.10+),
and the Docker Engine socket when present.
None of these are installed by the agent, and each one degrades to a stated behaviour rather than an error when it is absent:
| Tool | Used by | Without it |
|---|---|---|
ufw |
/firewall |
The view says it cannot manage this host's firewall, names the likely backends (firewalld, nftables, iptables) instead of claiming the host is open, and offers no rule-changing buttons. ufw is absent from Alpine, Arch, Debian netinst/cloud images and containers, and is EPEL-only on the RHEL family. |
| Docker Engine socket | /docker |
The container view and container alerts are disabled. |
journalctl |
SSH auth-log watching | Falls back to tail -F /var/log/auth.log. |
Development and the full test suite run on any OS (Linux, macOS, Windows) — every parser is a pure function tested against fixtures.
Requires Go 1.25+.
go test ./... # full suite (14 packages), green on any OS
go vet ./...
make build # cross-compile linux amd64 binaries into bin/
make dist # all release artifacts + checksumsSee CONTRIBUTING.md for setup, code-quality expectations, and how to structure a change, and docs/ARCHITECTURE.md for the design.
Generate a signing key once with go run ./tools/sign keygen, store the private
half as the STATIX_SIGNING_KEY repository secret, then push a v* tag — CI
builds, signs, and publishes the release.
Early open-source release (v0.x). The MVP described in mvp.md is built and covered by tests, and the project has iterated through several UX rounds (inline keyboards, live mode, button-driven watch management, alert action buttons, trends, and a first remediation action). It is a working project with a strong foundation, now opened to the community.
It is not yet declared production-ready: it has not been through broad real-world deployment across many distros, and there is no published security audit. Treat v0.x accordingly, and please report what breaks.
The decentralized design has deliberate costs:
- No cross-server correlation — the same IP attacking several of your servers is not seen as one pattern.
- No aggregated view — an "all servers" query means asking each bot.
- History is in memory — a restart loses SSH/alert history; there is no database, by design. Only the small list of known SSH key fingerprints is kept on disk, along with the bot-managed trusted IPs, and only once some source is trusted.
See ROADMAP.md for what is done, what is stabilizing, and what is planned. Everything beyond the current release is clearly marked as future work and is not a commitment.
Contributions are welcome — this is exactly the stage where they have the most impact. Good first areas include Linux distro compatibility, hardware/sensor coverage, SSH/security detection, tests, and documentation. Start with CONTRIBUTING.md.
Please do not open a public issue for security problems. Report privately via a GitHub Security Advisory. See SECURITY.md for the disclosure process.
StatixAgent is released under the MIT License. By contributing, you agree that your contributions are licensed under the same terms.
Thanks to everyone who has contributed to StatixAgent:

