Runs a command and keeps it alive only while valid pulses from the Cardputer keep arriving. Needs Python 3.12 with cryptography, plus pyserial (USB) and bleak (BLE). Create .venv next to the wrapper, or edit deadhand to use your interpreter:
python3.12 -m venv tools/deadhand/.venv
tools/deadhand/.venv/bin/pip install cryptography pyserial bleak
State (trusted key, used nonces, last pulse, log) lives in $DEADHAND_HOME, default ~/.config/deadhand.
deadhand pair [--source usb|ble] trust the device key (shows its fingerprint; once)
deadhand run --interval 40m --grace 5m [options] -- COMMAND ARGS...
deadhand listen --interval 40m --grace 5m receive pulses only; pair this with the hook
deadhand status is the last pulse still good?
deadhand hook Claude Code PreToolUse hook (reads stdin)
run options: --source ble|usb|file:PATH (default ble), --port, --id LABEL, --on-silence stop|pause|none, --on-silence-cmd 'script', --kill-after 10s, --notify, --log PATH, --quiet.
run, the clock is armed: you are at the keyboard, that counts as the first “I am here”.--interval. After that the grace period starts (the device turns red and blips); a press inside grace still saves the run.silence: no valid pulse for 45m (interval 40m + grace 5m); action: stop.run: the command’s own, or 75 if it was stopped for silence.A pulse counts only if: the ed25519 signature matches the trusted device key; its label, interval and grace equal what the host was started with (a process that talks to the device on its own cannot pick a longer interval); its time window is fresh; its nonce was never used; and its counter is above the last accepted one. Rejected pulses are logged with the reason.
--on-silence stop (default): SIGTERM to the whole process group, SIGKILL after --kill-after.--on-silence pause: SIGSTOP; the next valid pulse resumes the process (SIGCONT). Good for things that must not lose state.--on-silence none: only log (and run the command/notification below).--on-silence-cmd 'script': run any shell command; it gets DEADHAND_REASON and DEADHAND_SILENT_FOR in the environment. Use it to flatten a position, flush a queue, call a webhook, send yourself a message. Combine with any of the above.--notify: desktop notification when a pulse becomes due and on silence (macOS osascript or Linux notify-send).run stops a process. To also gate every tool call of an interactive or long agent session, run a listener and install the hook:
tools/deadhand/deadhand listen --interval 40m --grace 5m --on-silence none &
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "*", "hooks": [ { "type": "command", "command": "/abs/path/to/tools/deadhand/deadhand hook" } ] }
]
}
}
When the last pulse is older than interval + grace, the hook answers deny with a plain reason (the last human pulse was 47m ago (limit 40m + 5m grace). Press the button on the deadhand device to continue.). The hook trusts nothing but the signature: it re-verifies the stored pulse against the trusted key on every call, so editing the state file does not help. The agent must not be able to edit the hook configuration or the key file; protect them the way you protect any permission setting.
--source file:- reads pulses from stdin, and signcheck sign --kind pulse emulates the device with a test key:
signcheck/signcheck keygen --dir /tmp/dh-keys
export DEADHAND_HOME=/tmp/dh-home; mkdir -p $DEADHAND_HOME
tools/deadhand/deadhand pair --pub-file /tmp/dh-keys/test.pub.json --yes
# feed pulses (one JSON line each) into: deadhand run --source file:- --interval 1m ...
Tests: tools/deadhand/run-tests.sh (emulated device, no hardware needed).
The device key is not hardware-protected (see the firmware README). BLE and USB are only transports: they carry no secrets, and everything is verified on the host. If the host machine itself is compromised, so is the switch.