A Rust cockpit that lets an agent tune my keyboard's firmware — but never without my approval.
I type on a Skinner39 — a custom 39-key ZMK split keyboard I design and flash myself. ZMK is powerful but the tooling is fragmented: you edit layout at studio.zmk.dev, tune firmware timing in a .keymap file in git, kick builds through the GitHub Actions UI, and drag a UF2 file onto a bootloader drive by hand. Nothing tells you how you actually type, and nothing keeps those surfaces honest with each other.
Keysmith is my answer: a local-first native macOS app in Rust — a single cockpit that sees the live keymap, measures my typing privately on-device, lets an agent propose layout changes, and — only on my approval — applies them, either live over USB-RPC or by building and flashing new firmware.
The hard part isn't the UI. It's that there are two sources of truth that constantly drift apart, and an agent in the middle that must never be trusted blindly:
.keymap is truth-intent — the canonical design I've committed.If the agent proposes an edit against a stale mental model of the keyboard, it corrupts the thing I use to do all my work. So the whole system is governed by two non-negotiable invariants: reconcile drift before any mutation, and every keymap mutation requires explicit human approval.
I built it as a Rust workspace of six crates. keysmith-core is the shared engine — a .keymap parser (tree-sitter over the devicetree grammar), a byte-range write-back splicer, a wrapper over the ZMK Studio RPC control plane ( over USB-CDC serial), a candidate generator for the agent, and the piece that ties it together: a that reads the device over USB-RPC, parses git, and returns a verdict the rest of the system refuses to mutate through when it says . Around it sit an always-on daemon (), a GPUI cockpit (), and a generated design system ( / ).
zmk-studio-apiDriftStateDriftedkeysmithdkeysmith-cockpitkeysmith-themekeysmith-uiThe agent side is deliberately declawed. The candidate generator is inert by construction — it produces structured, scored proposals and cannot touch the device, the file, or git. A human strikes every blow. The one closed loop is observe → measure → propose → **human-approve** → apply → A/B → commit → re-observe.
This is a private, personal project: nothing is packaged or distributed. The root README still carries an early "Phase 0" headline, but the canonical build tracker and the code have moved well past it: the engine, the always-on daemon, and a working multi-tab GPUI cockpit (Home / Keymap / Reports / Coach / Repair / Devices) all build and run, verified live on my own machine. The closed loop runs on real captured telemetry — the always-on listen-only capture agent has logged 31k+ keystrokes of my own typing — and the first real proposal is sitting in the ledger pending my decision. The one piece that isn't done is the on-device OLED-glance firmware: built but unflashed, waiting on a soak-test gate.
The engine underneath is well tested: keysmith-core carries hundreds of unit tests over real keyboard fixtures (281 core tests green at the latest engine commit, workspace building warning-free), covering drift reconciliation, the write-back splicer (insert→delete provably restores the original bytes), the propose-only agent, and a correction-anchored shift-misfire estimator whose accuracy I validate against a labeled opt-in lab session with an explicit statistical-significance gate.
Working today: the .keymap parser and faithful splicer; a live USB-RPC link to my real board (an &kp Q↔&kp ESC edit applied live and read back byte-exact on hardware); a reconciler that catches behavior-type and structural drift; and an agent whose propose-only nature is enforced structurally — not by a code comment I promise to honor.
One known limit: v0 compares by behavior identity (does position (2,5) hold a "Key Press" on both sides?), which catches type-level and structural drift but not yet parameter equality — a Studio rebind of Q→ESC keeps the behavior "Key Press", so v0 reads it as in-sync. That gap is documented in the module header and folds in with the flash round-trip work. The interesting bugs live exactly where the model and the hardware disagree — which is the part I keep coming back to.