cardputer-signer

deadhand (host side)

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.

Commands

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.

How the clock works

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.

What to do on silence

Claude Code hook

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.

Trying it without the device

--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).

Limitations

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.