These tools answer questions about the machine itself. Why is the network down? What is this computer, exactly? Why does my terminal look the same on every PC? What does a laptop setting really do? The rule from Safety rails turns into a rule about measuring: say only what was measured, and say what was not.
net-doctor: the first broken layer wins
A network fails from the bottom up. A dead link makes the gateway unreachable, which makes DNS fail, which makes every name lookup fail. So net-doctor walks six layers in order and stops at the first broken one. The layers after it are never run, so they report "skipped", never a result of their own.
Here is a run on one of its test fixtures: a fake interface, documentation addresses, and a stub in place of every command:
$ network/doctor/doctor.sh == link == ✓ eth0 has carrier (up) == address == ✓ eth0 has IPv6 2001:db8::1a2b/64 (reported, not diagnosed) ✓ eth0 holds 192.0.2.50/24 (manager state: connected) == gateway == ✓ gateway 192.0.2.1 on eth0 answers ping == dns == ✗ resolver @192.0.2.1 does not answer ✗ resolver @192.0.2.2 does not answer == internet == skipped — blocked by dns == services == skipped — blocked by dns fault at dns: none of the configured resolvers answer (@192.0.2.1, @192.0.2.2) $ echo $? 1
Several of its rules exist because a simpler check would have given a confident wrong answer:
- "Dead" is a list, not "not up". VPN and tunnel interfaces have no carrier hardware, and they report
unknownwhile fully up and routing. A check that read "not up" as dead would fault a working machine and offer to delete the one route it needs. So an interface is dead only when it isdown,lowerlayerdown,dormantornotpresent. - Ask the real upstream. systemd-resolved points
resolv.confat its own local stub, and the stub answers whether or not the real server does. net-doctor reads the real upstream servers fromresolvectland queries those. - A rate, never a total. One counter read 807 TCP timeouts, which looked alarming until it was divided by 86 minutes of uptime. Sampled over 20 seconds, the live rate was 0.1%. Every kernel counter it reads is now a change over a sampling window.
- Only the destination's loss counts. Routers give low priority to the ICMP replies they generate themselves, so
mtrshowed 5–25% loss at hops that were forwarding everything. The doctor reads one number: the loss at the destination. - A false pass, caught live. On 2026-10-18, the first run against a real DNS fault reported the resolver as answering.
digprints its errors on stdout, even with+short, and the check counted any output as an answer. The test stub printed nothing for a dead resolver, so the suite never saw it. Now an answer means exit 0 and a line that is not a comment, and the stub printsdig's real text.
With --fix it repairs one confirmed step at a time. It refuses a remedy that is not marked safe when the session looks remote, or when the remedy would touch the interface the session runs on. It prints that remedy and does not run it, because a repair tool that locks you out of the box is worse than no tool. Where a network manager cannot repair one interface on its own, the doctor names the manager and offers nothing. A remedy that cannot run is worse than no remedy. And if it finds no checks to run at all, it exits 1, not with a clean bill. The suite has 50 fixture scenarios, and it passed 265 of 265 on 2026-10-25.
The system scan: what this machine is, measured
scan writes a machine's full spec, from eight collectors (identity, compute, storage, network, services, software, health and security), into one JSON file and a SPEC.md for people to read. SPEC.md closes with two blocks, Attention and Not measured. Attention is judged on seven rules, every one listed in the docs, and nothing else. Here is that block rendered from the suite's hand-written fixture, a fictional machine in every bad state at once:
$ python3 scan/system/render.py spec $ sed -n '/^## Attention/,$p' spec/SPEC.md ## Attention > Stable facts only: SMART, filesystems over 90 %, battery health, failed services, firewall, disk encryption, pending security updates. What could not be measured is under *Not measured*, not here. - SMART on `sda` (FAILING DISK) reports **FAILED**. - `/` is **97 % full** (14.2 GiB free). - Battery holds **62 %** of its design capacity (under 80 %). - **2 failed services**: `nginx`, `chronyd` - Firewall (`firewalld`) is **stopped**. - The system volume is **not encrypted**. - **3 pending security updates** (package metadata 52 h old). ## Not measured Re-run with `--sudo` to close the `needs-privilege` gaps, or run the commands below by hand. | Section | Field | Reason | Re-run | |---|---|---|---| | storage | `disks[1].smart` | needs-privilege | `sudo -n smartctl -d sat -a /dev/sdb` |
Two exclusions are deliberate:
- What could not be measured is never a finding. A disk that was not asked, for lack of privilege, goes under Not measured with the command to re-run. An alarm raised by a missing privilege would teach the reader to ignore the block.
- A value that describes one second is reported, never judged. A 91 °C reading sits in the health table and never in Attention.
The scan sends nothing over the network, and it writes nothing outside its own folder. An isolation test caught two probes that broke that promise. flatpak list creates a folder in the home directory on a machine that has never used flatpak. An unprivileged dnf -C check-update copied the whole system package cache, about 100 MB, into the home directory. Both probes were changed, because a scan plugged into someone else's machine for thirty seconds must not leave a package cache behind.
zconnect: one folder, every machine
I work across several PCs, on Linux and on Windows, and I want the same terminal on each. With zconnect you clone tool-kit and run zc doctor, and the machine works out from its hostname which connection it is. That connection becomes local, and the rest stay remote over SSH. Every device gets the same menu, with the same keys in the same order, local or remote, Linux or Windows.
None of it needs admin rights. Python comes from a pinned uv and CPython in 0-lib/, set up beside the package on the first run, with no change to PATH. zellij comes from the machine when it has one. Otherwise it comes from a pinned portable build, fetched once and checked against its checksum. Nothing is installed system-wide.
Each device gets its own zellij theme: the base theme with a new background and a new family of accent colours. Every palette is one row in one spec file. A generator derives each theme from a shared template, and its --check regenerates them all in memory and compares them with the files on disk:
Measured, not assumed
Two small laptop tools, each with a claim that turned out wrong once it was checked:
- A battery cap. A laptop that lives on its charger ages its battery at 100%, so a systemd unit caps the charge at 40%. How long the laptop runs on battery at that cap, if the power goes, was first worked out from an assumed draw of 10–15 W, which gave 66–99 minutes. Then the draw was measured from upower's own history: a median of 18.4 W and a 90th percentile of 23.8 W, over 148 samples. That leaves 41–53 minutes. A separate advisor reads that history and prints the install line for the cap it would choose, but changes nothing itself. Laptop firmware commonly resets the cap at power-on, and a sleep cycle may lose it too, so the unit runs again on every boot and every resume.
- The lid. What a closed lid does is decided by three layers, set in three places by three different mechanisms, and any one of them read alone gives a wrong answer. GNOME still lists two lid-action keys in its settings, and they look like live configuration. But the power daemon's binary contains neither name, so setting them does nothing at all. The lid tool reads all three layers and marks those two keys as dead. When a logind value is unset, it says what the value falls back to.