Boot a laptop that has no operating system from this USB stick and it walks you through testing every piece of hardware, then writes a verdict and a report back onto the stick.
Built for the situation where you are buying a second-hand laptop from a stranger and have ten minutes to decide whether it is sound. Nothing is installed and nothing on the target machine is modified — it runs entirely from the live session.
[ PASS ] BAT0 health
82% of design capacity (19.7/24.0 Wh)
[ FAIL ] /dev/sda - HEALTH
3 reallocated sectors; 1 pending (unstable) sectors
[ WARN ] eDP-1 panel resolution
Native resolution only 1366x768 - a laptop this size should
normally be 1920x1080. Possible downgraded/replaced panel.
DO NOT BUY / RENEGOTIATE
English, French and Arabic — the interface, the prompts and the HTML
report. Choose with --lang fr, $HWCHECK_LANG, or from the first screen of
the run. Arabic switches the report to right-to-left.
| Area | What it looks for |
|---|---|
| Identity | vendor/model/BIOS/serial sanity, chassis-vs-board vendor mismatch, traces of a previous OS, BitLocker |
| CPU | real model, cores, base vs turbo clocks, thermal throttling under sustained all-core load, peak temperature, fan RPM, kernel MCEs |
| RAM | total, slots used, module speeds, mismatched pairs, optional memtester pass (plus MemTest86+ on the stick) |
| Storage | SMART health, power-on hours, reallocated/pending/uncorrectable sectors, SSD wear, NVMe spare + critical warning, sequential read speed, kernel I/O errors |
| Battery | design vs actual capacity, cycle count, charging circuit, AC detection |
| Display | running vs native resolution, physical size, PPI, backlight control, full-screen dead/stuck-pixel test, backlight bleed, hinge-cable flicker |
| Audio | speakers L/R, distortion at volume, channel balance, microphone record + playback, headphone jack |
| Keyboard | every individual key (raw /dev/input capture), Fn/hotkeys |
| Touchpad | movement, buttons, edges, two-finger scroll, gestures, TrackPoint |
| Camera | device, formats, a real captured frame saved into the report |
| Network | wifi hardware + live scan, ethernet link, bluetooth, rfkill switches |
| USB | device enumeration, per-port plug/unplug events, over-current errors, stick speed |
| Sensors | temperatures, fan tachometer, ACPI zones |
| Chassis | cracks, hinges, missing screws, liquid damage, swollen battery (hard fail) |
| Extras | fingerprint reader, TPM, card reader, HDMI, Thunderbolt, optical drive |
Plus a per-machine verdict of GOOD, ACCEPTABLE, BUY WITH CAUTION or
DO NOT BUY / RENEGOTIATE, with hard failures listed separately from the
things worth renegotiating.
Ventoy multiboot USB + a Debian-based live ISO + this toolkit as a payload folder. The default ISO is SparkyLinux 8.4 Xfce (1.94 GB, Debian 13 "trixie"), chosen over a bigger desktop ISO because it is half the size while carrying a newer kernel (6.12 LTS), and it still ships a full Xfce session — which the screen tests need.
<Ventoy partition>/
├── sparkylinux-8.4-x86_64-xfce.iso boot entry 1: live desktop
├── MemTest86Plus-8.10.iso boot entry 2: full RAM test
├── HOWTO-USE.txt instructions for whoever holds the stick
└── hwcheck/
├── hwcheck.py the checker
├── i18n.py lang/ message catalogs (en, fr, ar)
├── RUN-HWCHECK.sh launcher: installs deps, runs as root
├── install-deps.sh fallback dependency installer
└── debs/ offline .deb bundle
Of ~20 external tools the checker invokes, Sparky's ISO already ships 15
(lm-sensors, alsa-utils, pciutils, usbutils, dmidecode, upower,
iw, rfkill, x11-xserver-utils, xdg-utils, …) and lacks 5:
smartmontools, memtester, v4l-utils, feh and eog. So the kit ships a
14-package, 17 MB offline .deb bundle and the launcher installs only the
packages the live system is missing — it can never clobber a working live
session. Once built, the stick needs no internet to be useful.
.
├── build-usb.sh writes the kit onto a USB stick (DESTRUCTIVE)
├── HOWTO-USE.txt instructions copied to the stick for the buyer
├── CHANGELOG.md release history
├── .github/workflows/ CI: privacy scan, catalogs, self-test, syntax
├── docs/PROJECT-LOG.md engineering log: decisions, defects, what is verified
├── docs/assets/ logo, README header, social card (SVG + PNG)
├── hwcheck/ the payload that ends up on the stick
│ ├── hwcheck.py the checker
│ ├── i18n.py lang/ translation layer + catalogs
│ ├── RUN-HWCHECK.sh launcher (trilingual)
│ ├── install-deps.sh fallback dependency installer
│ ├── build-deb-bundle.sh rebuilds the offline package set
│ └── build/ pristine English source, used by the i18n pipeline
└── tools/ build-machine tooling, NOT shipped on the stick
├── check_clean.py refuse to publish machine-specific data
├── check_translations.py catalogs match the source, and are complete
├── selftest.py load the i18n layer and exercise it
├── i18n_pipeline.py extract every display string into a catalog
└── integrate.py wire the catalog into the program
iso/, ventoy/ and the .deb set are not in the repository — they are
large binaries. See the next two sections to fetch them.
mkdir -p iso && cd iso
# SparkyLinux 8.4 Xfce 64bit (~1.94 GB) - Debian 13 "trixie" based, live desktop
# It ships fonts-dejavu-core/mono, which cover Arabic script, so the Arabic
# interface renders correctly without adding a font package.
curl -LO https://sourceforge.net/projects/sparkylinux/files/xfce/sparkylinux-8.4-x86_64-xfce.iso/download
mv download sparkylinux-8.4-x86_64-xfce.iso
curl -LO https://sourceforge.net/projects/sparkylinux/files/xfce/sparkylinux-8.4-x86_64-xfce.iso.allsums.txt/download
mv download sparkylinux-8.4-x86_64-xfce.iso.allsums.txt
# The allsums file has md5/sha1/sha256/sha512 sections, so pull the sha256 line
# and check it. (A naive `grep -A2 sha256 | tail -1` grabs a blank line and
# sha256sum errors with "no properly formatted checksum lines".)
grep -A1 '^# sha256sum:' sparkylinux-8.4-x86_64-xfce.iso.allsums.txt | tail -1 | sha256sum -c -
# must print: sparkylinux-8.4-x86_64-xfce.iso: OK
# expected: 7103af4d18741842202fca3cbe838855c51724d66563931c24522c0437796537
# The ISO's own installed-package list - the bundle builder subtracts it, which
# is what keeps the offline bundle at ~14 packages instead of ~200.
curl -LO https://sourceforge.net/projects/sparkylinux/files/xfce/sparkylinux-8.4-x86_64-xfce.iso.package-list.txt/download
mv download sparkylinux-8.4-x86_64-xfce.iso.package-list.txt
# MemTest86+ (~6 MB) - an independent boot entry with a newer build than the
# memtest this ISO already carries on its own boot menu.
curl -LO https://www.memtest.org/download/v8.10/mt86plus_8.10_x86_64.iso.zip
unzip mt86plus_8.10_x86_64.iso.zip
mv memtest.iso MemTest86Plus-8.10.isoAlways verify the checksums against the vendor's published values — a corrupted ISO produces a stick that fails to boot in ways that look like hardware faults.
# 1. Ventoy bootloader
mkdir -p ventoy && curl -L https://github.com/ventoy/Ventoy/releases/download/v1.1.17/ventoy-1.1.17-linux.tar.gz \
| tar xz -C ventoy
# 2. the offline .deb bundle (needs internet and a container engine, once)
./hwcheck/build-deb-bundle.sh --iso-list ../iso/sparkylinux-8.4-x86_64-xfce.iso.package-list.txt
# 3. write the stick (4 GB+ USB stick; the payload is ~2.0 GB)
./build-usb.sh --list # show candidate devices
sudo ./build-usb.sh /dev/sdX # write it - erases the stickbuild-usb.sh installs Ventoy, copies the ISOs to the root (Ventoy lists them
as boot entries), copies hwcheck/ to <USB>/hwcheck, drops the .deb bundle
and HOWTO-USE.txt, then verifies the result and unmounts cleanly.
It refuses to touch a device that does not look like USB/removable media unless
FORCE=1 is set, and makes you type the device name before erasing anything.
To stage the payload as a plain folder first — useful for inspecting it or zipping it up — no root and no device needed:
./build-usb.sh --target-dir /tmp/payload--refresh re-copies the payload onto an already-Ventoy-formatted stick
without repartitioning it.
./hwcheck/build-deb-bundle.sh --iso-list ../iso/sparkylinux-8.4-x86_64-xfce.iso.package-list.txtTwo design decisions keep this small and correct:
-
--iso-listsubtracts what the ISO already ships. Without it you get the whole dependency closure (~250 packages). With it you get the genuine difference: 14 packages, 17 MB. Always pass the ISO's own.package-list.txt— Sparky publishes one next to the ISO, and it is also inside the image at/live/. -
The closure is resolved inside a container of the target distro (
debian:trixievia podman or docker), so package versions always match the ISO. Building against the wrong base — Ubuntu debs for a Debian ISO — produces packages that will not install. If no container engine is available the script falls back to the host and warns that this is only correct when the host is the target distro.
Verify the result before shipping: every dependency of every bundled package
must be satisfied by the bundle itself, by a virtual Provides:, or by a
package in the ISO's list. That is what makes the offline install work on a
machine with no internet.
Pitfall that produced a 193-package bundle: the ISO package list prints architecture-qualified names for
Multi-Arch: samepackages (libc6:amd64), whileapt-cache dependsemits plain names. Comparing them directly matched almost nothing, so nearly the entire closure was shipped. Strip the:archsuffix when building the "already installed" set.
- Plug it in, power on, tap the boot-menu key (Lenovo/Dell/Acer
F12, HPF9, AsusEsc). - Pick the Sparky ISO in the Ventoy menu → live desktop (1–2 minutes).
- Open the stick in the file manager →
hwcheck→ right-clickRUN-HWCHECK.sh→ Run in Terminal. - Answer the prompts. First run installs the bundled tools offline (~30 s).
- Read the verdict. The report is saved under
<USB>/report/<date>_<model>/.
HOWTO-USE.txt on the stick is the same instructions written for a
non-technical buyer, including per-brand boot keys and a troubleshooting list.
Each run writes one folder, <date>_<model>, with a -2, -3… suffix if you
test twice in the same minute. It contains report.html (a printable page),
report.txt, report.json and the captured webcam frame — so the machine's
state on the day of sale is on the record.
The launcher is translated as well (banner, install progress, errors) and passes the language through:
sudo ./RUN-HWCHECK.sh --lang arFor a full RAM test, reboot and pick MemTest86Plus from the Ventoy menu.
Let it complete at least one pass (30–60 min). Any red error means walk away.
The in-session memtester check is only a quick sample.
cd hwcheck
python3 hwcheck.py --quick --auto # no prompts, automatic checks only
python3 hwcheck.py --quick --lang fr # guided, skipping the slow tests
python3 hwcheck.py # full run
python3 hwcheck.py --sections cpu,storage,battery
python3 hwcheck.py --report-dir /tmp/out # where the report goesRun it as root for the full set: dmidecode, smartctl and the raw
/dev/input keyboard capture need it. Without root the program degrades
gracefully and says so in the report.
--auto exists so the whole suite can be smoke-tested non-interactively on a
known-good machine — which is how the false-positive bugs listed below were
found. A healthy laptop must come out GOOD, not FAIL.
lang/en.py is the source language and the single source of truth. Each other
catalog defines exactly the same key set:
MESSAGES = {"cpu.peak_temperature": "Peak temperature", ...}
SECTIONS = {"Storage": "Stockage", ...} # internal id -> display label
STATUSES = {"FAIL": "ÉCHEC", ...} # internal code -> short labelRules that will break the program if ignored:
- never rename or delete a key, never invent one;
- never delete or rename a
{v0}…{vN}placeholder — moving it elsewhere in the sentence is fine and often necessary, since word order differs; - values are substituted verbatim, so keep the unit that appears in the English
string (
GB,Wh,MHz,%); - section ids (
Storage,CPU, …) and status codes (PASS,FAIL, …) are logic keys — translate only the values inSECTIONS/STATUSES.
Check a catalog before committing:
python3 tools/check_translations.pyIt verifies the key sets match, the placeholder sets match, and that no translation is an empty string or a verbatim copy of the English.
hwcheck.py calls tr("key") for every user-visible string. Those call sites
were generated once from the plain-English original by tools/i18n_pipeline.py
tools/integrate.py, using AST spans rather thanast.unparseso that comments and formatting survive. The pristine English input is kept inhwcheck/build/so the migration can be re-run.
You do not need the pipeline to add a language — add a catalog. You only need it if you want to re-extract strings from a changed English source.
Named tr() and not t() on purpose: several checker functions assign to a
local t (temperatures, thread handles), which would shadow the translator and
raise UnboundLocalError at the call site.
Three tools gate a release, and all three run automatically on every push and
pull request (see .github/workflows/checks.yml):
python3 tools/check_clean.py # must print CLEAN - no private data
python3 tools/check_translations.py # catalogs match the source
python3 tools/selftest.py # the i18n layer loads and resolvescheck_clean.py is the one that matters most here. This tool records
machine identities by design, so its own test output is the easiest thing to
leak: serials, BIOS versions, MAC addresses, disk models, battery data. It scans
every committed file for absolute home paths, MAC/IP addresses, e-mail
addresses, DMI fingerprints and serial assignments. report/, iso/,
ventoy/ and *.deb are gitignored for the same reason. Add your own machine's
fingerprints to its FORBIDDEN list.
CI also compiles every Python file, bash -n-checks every script, and asserts
that all fourteen check sections are actually referenced in the run plan — a
section that exists but was never wired in would silently never run.
Release history is in CHANGELOG.md.
- Thermal throttling must be judged against the CPU's base clock
(
/sys/.../cpufreq/base_frequency), notcpuinfo_max_freq. An all-core load never reaches the single-core turbo ceiling, so comparing against turbo reports healthy machines asFAIL. sensorsoutput containshigh = +100.0°Climit columns; a naive temperature regex reads those as readings and reports a 100 °C idle machine. Skip lines containinghigh =,crit =,min =,max =,low =.- A panel running below its native resolution is not a fault. The fraud signal is a low native resolution or a missing EDID preferred mode.
iw scanneeds root; a non-root run only gets "Operation not permitted".- In Python 3.11 an f-string's
format_specnode inherits the position of the enclosing f-string, soast.get_source_segmenton it returns the whole literal instead of".0f". Rebuild the spec from the node's constants. - Installing a whole
.debbundle over a live session can overwrite core libraries. Install only packagesdpkg -ssays are missing. - Ventoy makes an exFAT partition labelled
Ventoyplus a small EFI one. Find the payload by label and by marker files, and mount it yourself if the live session did not automount it. - Dead-pixel testing needs a fullscreen viewer:
fehis bundled, witheogandxdg-openas fallbacks. Test images are written by a pure-python zlib+struct PNG encoder, so no ImageMagick or PIL is needed.
Software cannot see a warped chassis, a worn hinge, missing screws or a
swollen battery. The Chassis section asks you to check those by hand — a
swollen battery is a hard fail and a fire risk. HOWTO-USE.txt lists the full
manual checklist, including comparing the BIOS-reported CPU/RAM/disk against
the advertisement.
MIT — see LICENSE.
