A hardware approval signer for AI agents, running on the M5 Cardputer ADV.
When an agent wants to do something risky (for example git push origin main), it sends the exact action text to the device. The device shows the text on its screen, and only a physical button press by a human produces a signature. The agent-side hook then verifies the signature with signcheck and proceeds only on an explicit “yes”. No answer, a timeout, or any verification failure means “no”.
pubkey command).signcheck verify checks the signature, that kind/id/text match what the agent asked, the time window, and that the nonce was not used before. Altered text is rejected as bad_sig; a re-submitted answer is rejected as replay.firmware/signer/ - PlatformIO firmware for the Cardputer ADV.signcheck/ - verifier, a device emulator for tests (signcheck sign, test keys only), and tests.tools/cardputer-sign - host client: sends a request to the device and verifies the reply. --save-response PATH stores the signed reply.firmware/deadhand/ - the dead man’s switch firmware; tools/deadhand/ - its host side (deadhand run|listen|hook|pair|status).tools/forgery-check - takes a saved reply, checks that tampered text gives bad_sig and a repeat gives replay.cd firmware/signer
pio run -t upload --upload-port /dev/cu.usbmodemXXXX
Needs PlatformIO. Dependencies (M5Cardputer, M5Unified, ArduinoJson, NimBLE-Arduino, Crypto) are in platformio.ini.
Create a Python 3.12 venv with cryptography (and pyserial, bleak for the device client) as .venv next to the wrappers, or edit the wrappers to point at your interpreter.
tools/cardputer-sign pubkey --save # trust the device key once
tools/cardputer-sign sign --kind perm --id demo-1 --text "git push origin main"
signcheck/run-tests.sh # needs pytest in signcheck/.venv
A second firmware for the same hardware. A long-running process (an overnight agent run, a render, a trading bot) keeps going only while you press the side button every N minutes. Each press is a signed pulse; if the host stops seeing valid pulses, it stops the process and logs why. Because the key is on the device and only the button uses it, the watched process cannot extend its own leash.
# one-time: build, flash and trust the key
(cd firmware/deadhand && pio run -t upload --upload-port /dev/cu.usbmodemXXXX)
tools/deadhand/deadhand pair --source usb
# run anything under the switch: press the button every 40 minutes, 5 minutes of grace
tools/deadhand/deadhand run --interval 40m --grace 5m -- ./overnight.sh
Examples: an overnight coding-agent session, a long script or data migration, a trading bot that should not run unattended, a render farm job, a model training run on rented GPUs, a robot or lab rig, a “while I am at my desk” access window. For Claude Code there is also a PreToolUse hook that refuses tool calls when the last pulse is too old. Details: firmware/deadhand/README.md (device, settings) and tools/deadhand/README.md (host side).
See signcheck/FORMAT.md for the signed message format and verification rules.
This is a prototype. The private key is stored in ESP32 NVS in plain form: no flash encryption, no secure boot, no eFuse protection. Anyone with physical access and a USB cable can extract it. Do not treat it as a production trust anchor. Keys marked test: true are rejected for the go kind.
MIT, see LICENSE.