Documentation
picket is a host threat-hunter. It pushes a read-only sweep to each server in your inventory over SSH, scores what it finds, and — only when you arm it — freezes the unambiguous malware. This page covers the host surface; the identity engine (picketd) lives in the repo.
install
curl -fsSL picket.sh | sh
macOS (Apple silicon) and Linux (x86_64) get a prebuilt static binary. Any other target builds from source, which needs Rust. The script drops a single picket binary into /usr/local/bin (or ~/.local/bin if that isn't writable) and touches nothing else. The binary the controller runs is the only thing installed; the sweep itself is shipped to each host over SSH at runtime, so there is nothing to install on the boxes you watch.
inventory
picket reads inventory.toml from the working directory (or --inventory PATH). Each host is an SSH alias from your ~/.ssh/config — the controller shells out to the system ssh, so it inherits your keys, ProxyJump, and ControlMaster.
# inventory.toml
[settings]
armed = false # observe-only until you trust the calls
ssh_timeout_secs = 45
[[host]]
ssh = "build-host"
note = "coolify — runs untrusted app containers"
[[host]]
ssh = "hetzner"
note = "primary web + app services"
run
| command | what it does |
|---|---|
| picket run | observe-only sweep of every host in the inventory |
| picket run --host NAME | sweep a single host |
| picket run --armed | allow auto-containment of slam-dunks for this run |
| picket run --json | print the raw fleet report as JSON |
The exit code is 1 if anything alertable was found and 0 if the fleet is clean, so a timer or CI wrapper can page on it.
$ picket run
=== build-host === load 0.11 ✓ clean
=== hetzner === load 2.54 ✓ clean
=== krawler === load 0.02 ✓ clean
arming & the disposition rule
Every finding carries a severity and a slam_dunk flag, both decided on the host where the live process context exists. The controller only ever narrows from there — it never upgrades a finding, and never auto-acts on anything ambiguous. That rule is unit-tested.
| finding | armed | action |
|---|---|---|
| slam-dunk | yes | snapshot evidence, freeze, alert |
| slam-dunk | no | alert only ("would contain") |
| high / critical, not slam-dunk | any | alert only |
| info / low / medium | any | recorded, not surfaced |
Start with armed = false. Watch a few runs, confirm the slam-dunk calls look right, then flip it.
what it checks
- Process from a deleted binary in a non-system path — the miner's exact shape (
/tmp/BCZfwZZr (deleted)). A deleted/usr/bin/dockerdafter an apt upgrade is recognised as benign and never alerts; the discriminator is the path, not the name. - Sustained-CPU process working out of
/tmp,/dev/shm, or/var/tmp. - Outbound connection to a mining/C2-associated port from a public peer.
- Executable in a scratch dir (
/tmp,/var/tmp,/dev/shm). /etc/ld.so.preloadpresent — the classic userland-rootkit hook.- root
authorized_keysinventory (fingerprints, to diff against a baseline). - External SSH logins — accepted publickey/password from public IPs.
Two thresholds are env-tunable: PICKET_CPU_HOT (default 50) and PICKET_BAD_PORTS.
containment
Containment runs only for slam-dunks, only when armed, and is reversible. It snapshots before it touches anything — recovering the deleted binary from /proc/<pid>/exe, capturing the environment, open sockets, and (for a container) docker inspect/top — into /root/incident-<ts>/. Then it freezes:
- Container →
docker update --restart=nothendocker stop, so neither Docker nor an orchestrator revives it. - Bare process →
SIGSTOP, notSIGKILL. The process is frozen — no more work, no more network — but stays in the table for analysis and can be resumed if the call was wrong.
A false call costs you a frozen process and an evidence directory, never a service you can't explain.
scheduling
The repo ships systemd/picket.service + picket.timer — a oneshot that wakes every 15 minutes on a controller host that has SSH to the fleet.
cp picket /usr/local/bin/
mkdir -p /etc/picket && cp inventory.toml /etc/picket/
cp systemd/picket.{service,timer} /etc/systemd/system/
systemctl enable --now picket.timer
llm triage
Set PICKET_TRIAGE_CMD and, when a sweep surfaces anything alertable, picket pipes the full fleet JSON to that command's stdin and prints its assessment. The model sees only collected JSON — never a shell, never the hosts. This is the "model only on hits" path: the deterministic sweep is free every cycle, the model is paid for only when something is found.
PICKET_TRIAGE_CMD='claude -p "Triage these host-security
findings — real compromise vs benign ops noise?"' \
picket run
the identity surface
picket's other half is picketd: a behavioral risk engine that step-ups suspicious signins. It's a better-auth plugin running a three-stage pipeline — a deterministic scorer, an online Gaussian, and a prompt-injection-hardened LLM judge — with AEAD-sealed signatures and a signed-WASM plugin sandbox. It shares picket's spine: deterministic on the hot path, an LLM only for the ambiguous case, nothing uncertain auto-acted on. See the repo for setup.