deadmans-switch
v0.1.6
Published
A dead man's switch for autonomous agent fleets. Finds the agents that went quiet, and the ones that lie about succeeding.
Maintainers
Readme
Deadman's Switch
A dead man's switch for autonomous agent fleets.
Your agents don't crash. They go quiet, or they return ok: true having done
nothing, and you find out five weeks later.
npx deadmans-switch scanNo install, no account, no config, no database. It reads what's already on your machine and tells you which agents stopped — and, more usefully, which ones stopped for a reason you already know about.
What it looks like
Real output from a 29-job laptop fleet, run through --anonymize:
DEADMAN'S SWITCH 29 jobs · 7 projects · workstation
CHECK
● quarry · abandoned
missed 71 runs (silent 4 days), and nothing else here explains why
✗ quarry-queue missed 71 runs (silent 4 days)
● ember · abandoned
1 stopped, 1 never ran at all — no shared stopping point, so this was never one decision
✗ ember-job-1 silent 29 days · not loaded in launchctl
∅ ember-snapshot has never produced output · Disabled=true in plist
DELIBERATE — NO ACTION
● nimbus · paused
all 9 jobs went quiet within 11 days of each other, with no errors — this looks deliberate
5 of these have never run at all
HEALTHY
● meridian · active
all jobs producing on schedule
4 projects to check. Nothing on fire.
1 paused — not counted above.Those nine nimbus jobs have 971 missed runs between them. A conventional
monitor shows you 971 alarms, or nine red lights. This shows you one grey line,
because a human deliberately shelved that project — and then puts the one job
that actually stopped without explanation at the top.
The idea
The unit of judgment is the project, not the job.
A job that stopped is not news. A job that stopped while its siblings kept running is news — the living siblings prove the machine, the network and the scheduler all work, so the dead one has no excuse.
A whole project that stopped, all at once, with no errors, is usually a human deciding to work on something else. Reporting that as an incident is how a monitoring tool teaches you to ignore it.
| State | Meaning | Shown as |
|---|---|---|
| lying | Meeting its schedule and producing nothing. Green everywhere you'd think to look. | NEEDS YOU NOW |
| failing | Whole project dark and left errors behind — one credential or dep broke for everyone at once. Masquerades as a pause and costs weeks. | NEEDS YOU NOW |
| partial | Some jobs alive, some dead. Highest signal-to-noise finding there is. | NEEDS YOU NOW |
| zombie | Mostly stopped, stragglers still firing into the void and burning spend on work nobody reads. | CHECK |
| paused | Everything stopped together, quietly. A human decided this. Said once, collapsed, then quiet. | DELIBERATE |
| completed | Bounded run finished as designed. Not dead. | DELIBERATE |
| active | Running as promised. | HEALTHY |
What it detects
| Rule | Signature |
|---|---|
| Missed runs | Declared schedule vs. observed output — "declares every 3 h, has missed 452 runs", not "quiet for a while" |
| Credential expiry | 401, 403, invalid_api_key, billing disabled in run output |
| Green but empty | Runs on time, reports success, and stops producing. Needs a runner that records liveness and success separately, so today that means Hermes — an empty stdout is not evidence, because a careful script logs to its own file |
| Zombie path | The job invokes a binary that is no longer on disk — it cannot have worked, whatever its logs say |
| Never ran | Scheduled, but has never once produced a byte |
| Configured but inert | Plist on disk with Disabled=true, or absent from launchctl list |
| Zombie spend | Stragglers still firing into a project nobody is reading |
| Bounded and done | RRULE COUNT exhausted — finished, not dead |
Thresholds are always relative to a job's own schedule. Seven days of
silence is catastrophic for a 90-minute job and unremarkable for a weekly one.
When no schedule can be parsed, the verdict falls back to elapsed time and is
labelled inferred — never assumed healthy. Unmeasurable silence is the most
dangerous kind, and defaulting it to green is how a 61-day-dead agent stays
invisible.
Where it looks
Codex automations · Hermes cron and profiles · Claude scheduled tasks ·
launchd agents · crontab. Failures in one source never sink the scan.
macOS. A systemd adapter for Linux exists in the source and passes its
tests, but it has never been run against a real systemd, so it isn't claimed as
supported. node cli/systemd.test.js on a Linux box is what would change that.
Clustering, and why there's almost no config
Jobs are grouped by what their definitions say they operate on — repository
checkouts, tracker keys, team names — not by how they're spelled. atlas-board-driver
and northwind-security-scan land in the same project because both point at
the same checkout, which no amount of name-matching would tell you.
That gets most of a fleet with nothing configured. It has a floor: one real job's
entire body is Clear the linear triage queue, which identifies no project to
anyone but its author. When that happens the scan says so rather than quietly
filing each one as its own healthy project:
4 claude jobs name no repo, tracker or team, so each stands alone:
build, queue, triage, sweep
If some are one project, group them in .deadmanrc — see --help.Optional .deadmanrc, nearest one wins, searched from the working directory up:
{
"projects": {
"atlas": ["atlas-", "nightly-ingest", "warehouse-sync"]
}
}Patterns match the start of a job name. Grouping is all this file can do — it cannot set a severity, mark anything healthy, or silence a finding. A monitoring tool whose config can hide a dead job will eventually be used to hide a dead job.
Menu bar
A scan takes ~120 ms, which is fast enough to live in the menu bar.
npx deadmans-switch install-menubarThat finds SwiftBar or xbar, reads the plugin folder it's actually configured to watch, installs the plugin and refreshes it. One of those two apps has to be installed — the plugin is a script they run — and if neither is, the command tells you the one line to run and stops.
It is the only thing here that writes anything. It writes one file, to a plugin folder you just asked it to write to. Scanning still never touches a job, a plist or a crontab.
When nothing needs you it renders a single dim · and costs no attention. When
something does, it becomes ✗ 1, and the dropdown carries the same reasoning
the terminal gives you:
⚠ 1
─────────────────────────────
1 project needs you now
23 jobs · 4 projects · workstation
─────────────────────────────
NEEDS YOU NOW
● atlas · partial
1 of 13 jobs stopped while 12 kept running — the
siblings prove nothing is wrong with the environment
✗ dispatch missed 71 runs (silent 4 days)
∅ reconcile has never produced output
─────────────────────────────
9 paused · 13 healthyThe whole finding is on screen the moment the menu opens. Nesting the reasoning into a submenu is the obvious way to build this and the wrong one: it hides the only thing the tool has to say behind a hover.
A monitor you have to remember to open is one more thing that goes quiet, which would be a funny way for this particular tool to fail.
The plugin runs from $HOME, so put a .deadmanrc there if you want your
groupings applied — the one in a project directory won't be found.
Options
--json print the report as JSON instead of a dashboard
--out <path> write the report to <path>
--all show healthy projects too, not just what needs you
--anonymize stable aliases, roles preserved, all free text dropped
--menubar render for SwiftBar/xbar
--include-vendor include Apple/Google/Adobe launchd agents
--strict exit non-zero when anything critical is found--strict makes it a CI gate. --out plus the viewer merges reports from
several machines into one fleet view, which is why there is no SSH code in here
— same outcome, none of the transport failure modes.
Read-only, always
It never writes to, disables, kills or restarts anything it inspects. Suggesting a fix command is fine; running it is not.
--anonymize exists because the scan output is genuinely useful to share and
genuinely full of your infrastructure. It drops free text entirely rather than
scrubbing it — redacting paths and hostnames out of prose with regexes is a game
you lose eventually, and none of that text is load-bearing for the findings.
Requirements
Node ≥ 18. No dependencies.
License
MIT
