diagnose-hid-keycodes · git:20260422.42975e4 · 2026-04-22 · sha256 b886ee7777b986d0

diagnose-hid-keycodes git:20260422.42975e4A

Immutable. This exact content never changes and is served at /api/v1/blob/b886ee7777b986d0.

---
name: diagnose-hid-keycodes
description: Find out what an unknown USB/Bluetooth HID button actually emits without assuming the label or documentation. Uses Karabiner's no-op diagnostic rule + Karabiner-EventViewer + Quartz focus-free screen capture. TRIGGERS - what does this button emit, mystery HID keycode, macro pad keycode, 3-key pad keycode, Karabiner EventViewer, identify macro pad keycode, HID descriptor unknown, vk_none diagnostic, Karabiner device grab, inspect external keyboard keycodes.
allowed-tools: Read, Edit, Write, Bash
---

# Diagnose HID Keycodes

Given an unknown macro pad, mouse button, foot pedal, or HID gadget, find out exactly what each button emits at the OS level — **without guessing from labels, vendor docs, or photos**. Cheap HID pads frequently ship with arbitrary or mis-labeled keycodes (the Jieli/Free3-P ships with buttons labeled top/middle/bottom emitting Ctrl+C/Ctrl+V/Ctrl+X — which isn't the cut/copy/paste convention; it's hardware-random).

## When to Use This Skill

- A new HID device arrived and you don't know what its buttons emit
- A pad has multiple firmware modes and you need to map each mode's keycodes
- A rule isn't firing and you suspect you guessed the wrong `from.key_code`
- You need to document a device for a reproducible setup

## The Three-Tool Workflow

| Tool                      | Purpose                                                 |
| ------------------------- | ------------------------------------------------------- |
| Karabiner `ignore: true`  | Make Karabiner _observe_ the device without grabbing it |
| Karabiner-EventViewer     | Display raw HID events as text                          |
| Quartz `screencapture -l` | Capture EventViewer's window without stealing focus     |

`ignore: true` is the key insight: with it enabled, Karabiner doesn't remap anything but still logs the device's events — so you can see the raw keycodes the firmware emits.

## Workflow

### Step 1 — Identify the device's VID/PID

```bash
# USB
ioreg -p IOUSB -l -w 0 | grep -B 2 -A 6 "<product name or partial>"

# Bluetooth (after pairing)
system_profiler SPBluetoothDataType | grep -A 15 "<pad name>"
```

Record VID/PID in **decimal** (Karabiner's JSON format).

### Step 2 — Add a no-op diagnostic rule (forces Karabiner to grab the device)

**Why not just `"ignore": true` in `devices[]`?** That tells Karabiner to leave the device entirely alone — EventViewer then won't see its events either. `ignore: true` is for "hands off this device," not "inspect this device."

**Correct approach**: add an inert `complex_modifications` rule scoped to the device. Karabiner grabs the device (so EventViewer captures every HID report) but the rule does nothing. Edit `~/.config/karabiner/karabiner.json` → profile 0 → `complex_modifications.rules` and insert:

```json
{
  "description": "[DIAGNOSTIC] Grab <pad> (no remap)",
  "manipulators": [
    {
      "type": "basic",
      "from": { "key_code": "vk_none" },
      "to": [{ "key_code": "vk_none" }],
      "conditions": [
        {
          "type": "device_if",
          "identifiers": [{ "vendor_id": 19530, "product_id": 16725 }]
        }
      ]
    }
  ]
}
```

`vk_none` is a Karabiner virtual key that never matches real input, so the manipulator is inert. The `device_if` scoping makes Karabiner grab the device for inspection.

Reload Karabiner: Karabiner-Elements menu bar icon → Restart Karabiner-Elements.

### Step 3 — Open EventViewer and press each button

```bash
open -a "Karabiner-EventViewer"
```

- Main tab: shows `key_down` / `key_up` with decoded keycode names (`c`, `left_control`, `page_up`, etc.)
- Devices tab: shows which device emitted each event — confirms you're grabbing the right VID/PID
- Unknown Events tab: shows events Karabiner couldn't decode — relevant for consumer keys or custom HID descriptors

Press each button slowly. For modifier-combos emitted in one HID report (common on cheap pads), you'll see multiple key_down events in tight sequence:

```
13:44:02.123  key_down  left_control
13:44:02.123  key_down  c
13:44:02.198  key_up    c
13:44:02.198  key_up    left_control
```

Same microsecond timestamp for `left_control` + `c` = emitted in one HID report → you need `simultaneous` matcher.

### Step 4 — Capture without stealing focus

If you bring EventViewer to the foreground to read it, you lose the ability to press buttons on the test window. Workaround — capture by window ID:

```bash
# List windows; find EventViewer's window ID
python3 -c '
from Quartz import CGWindowListCopyWindowInfo, kCGWindowListOptionAll, kCGNullWindowID
for w in CGWindowListCopyWindowInfo(kCGWindowListOptionAll, kCGNullWindowID):
    if "EventViewer" in w.get("kCGWindowOwnerName", "") or "EventViewer" in w.get("kCGWindowName", ""):
        print(w["kCGWindowNumber"], w.get("kCGWindowName"))
'

# Screenshot that window without focusing it
screencapture -l <WID> -o -x /tmp/eventviewer.png
```

`-l <WID>` captures a specific window, `-o` excludes shadow, `-x` suppresses the capture sound. The window does not need to be foregrounded.

### Step 5 — Repeat for each firmware mode (Bluetooth pads)

Many cheap BT pads have undocumented firmware modes triggered by button combos (hold all 3 keys 5s, hold top 10s, etc.). Each mode can emit completely different keycodes. For each mode you discover:

1. Switch the pad into that mode
2. Repeat step 3 — log keycode for each button
3. Document in a table

Example (Jieli/Free3-P):

| Mode | Top                | Middle             | Bottom                  |
| ---- | ------------------ | ------------------ | ----------------------- |
| 1    | `volume_increment` | `volume_decrement` | `spacebar` (play/pause) |
| 4    | `page_up`          | `page_down`        | `equal_sign`            |

### Step 6 — Clean up

Remove the `[DIAGNOSTIC]` rule from `complex_modifications.rules` and reload Karabiner. Or convert it into your real remap rule by replacing `vk_none` with the actual `from` / `to` bindings.

## Avoid Touch-ID-Triggering Audits

Do NOT query TCC.db or SQLite files under `/Library/Application Support/com.apple.TCC/` to "audit permissions" during this workflow — those queries require sudo and trigger the Touch ID prompt on every invocation. Instead:

```bash
# Non-sudo audit: is Karabiner actually grabbing the device?
karabiner_cli --list-connected-devices | jq '.[] | select(.product == "<pad-name>")'
# Returns { ..., "is_grabbed": true/false } — same info, no biometric prompt
```

The working tool IS the audit. This was discovered the hard way; see [`../configure-macro-keyboard/references/04-anti-patterns.md`](../configure-macro-keyboard/references/04-anti-patterns.md) → "Sudo-based TCC.db audits trigger Touch ID".

## Deep References

- [`../configure-macro-keyboard/references/03-patterns.md`](../configure-macro-keyboard/references/03-patterns.md) — "`ignore: true` diagnostic" + "Quartz window-ID capture" patterns in full
- [`../configure-macro-keyboard/references/04-anti-patterns.md`](../configure-macro-keyboard/references/04-anti-patterns.md) — `{"any": "key_code"}` at top-level fails silently; position-inference mistakes
- [`./references/diagnostic-workflow.md`](./references/diagnostic-workflow.md) — expanded step-by-step with screenshots

## Sibling Skills

- [`configure-macro-keyboard`](../configure-macro-keyboard/SKILL.md) — once you know what your buttons emit, use this to write the device-scoped Karabiner rule. The `vk_none` no-op rule from Step 2 here converts directly into the real rule by swapping `from` / `to` bindings.
- [`emit-fn-key-on-macos`](../emit-fn-key-on-macos/SKILL.md) — if one of the keycodes you discovered should be remapped to real Fn (for Typeless, dictation, globe key), this sibling skill explains the one correct Karabiner incantation.