
Diagnose Hid Keycodes
- 40 installs
- 62 repo stars
- Updated August 3, 2026
- terrylica/cc-skills
Helps with ai & agent building tasks.
About
diagnose-hid-keycodes is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- diagnose-hid-keycodes
- AI & Agent Building
- AI-coding skill
Diagnose Hid Keycodes by the numbers
- 40 all-time installs (skills.sh)
- Ranked #8,266 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/terrylica/cc-skills --skill diagnose-hid-keycodesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 62 |
| Last updated | August 3, 2026 |
| Repository | terrylica/cc-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Diagnose HID Keycodes
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed — fix this file immediately, don't defer. Only update for real, reproducible issues.
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
# 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:
{
"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
open -a "Karabiner-EventViewer"- Main tab: shows
key_down/key_upwith 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_controlSame 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:
# 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:
# 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 promptThe working tool IS the audit. This was discovered the hard way; see `../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` — "
ignore: truediagnostic" + "Quartz window-ID capture" patterns in full - `../configure-macro-keyboard/references/04-anti-patterns.md` —
{"any": "key_code"}at top-level fails silently; position-inference mistakes - `./references/diagnostic-workflow.md` — expanded step-by-step with screenshots
Sibling Skills
- `configure-macro-keyboard` — once you know what your buttons emit, use this to write the device-scoped Karabiner rule. The
vk_noneno-op rule from Step 2 here converts directly into the real rule by swappingfrom/tobindings. - `emit-fn-key-on-macos` — 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.
Post-Execution Reflection
After this skill completes, reflect before closing the task:
0. Locate yourself. — Confirm this SKILL.md is the canonical file before any edit. 1. What failed? — Fix the instruction that caused it. 2. What worked better than expected? — Promote to recommended practice. 3. What drifted? — Update vendor IDs, keycodes, or FOSS-tool versions if reality disagrees with the doc. 4. Log it. — Add an evolution-log entry (or 04-anti-patterns.md row) with trigger, fix, evidence.
Do NOT defer. The next invocation inherits whatever you leave behind.
diagnose-hid-keycodes Skill
Find out what a mystery HID button actually emits, without guessing. Combines Karabiner'signore: true(orvk_none) diagnostic rule withKarabiner-EventViewerand Quartz focus-free screen capture so you can press a button and screenshot its raw event without losing focus.
Hub: Plugin CLAUDE.md | Sibling skills: configure-macro-keyboard · emit-fn-key-on-macos
What This Skill Owns
| File | Role | Edit policy |
|---|---|---|
SKILL.md | Diagnostic workflow: temporarily un-grab the device, watch EventViewer, screenshot via Quartz to keep focus on the pad. | Edit when a new diagnostic technique proves useful — e.g., a new way to capture multi-report HID events. |
references/diagnostic-workflow.md | Step-by-step with Case 1/2/3 edge cases (single-report combos, consumer keys, multi-interface devices). | Add new edge cases here. Each case should reference a real device that exhibited the behavior. |
Critical Invariants
1. `ignore: true` un-grabs the device but leaves Karabiner running. This is the key trick — without it, Karabiner intercepts the raw events before EventViewer can show them. Don't try to disable Karabiner globally during diagnosis. 2. Always remove `ignore: true` after diagnosis. Otherwise your remap won't fire (Karabiner won't grab the pad). The configure-macro-keyboard SKILL.md has a callout for this. 3. Use Quartz screen capture, not the macOS screenshot app. The macOS screenshot app steals focus, which causes the pad to start emitting events to whatever stole focus instead of EventViewer. The Quartz approach in references/diagnostic-workflow.md is focus-free. 4. Cheap pads emit modifier+key in one HID report. EventViewer will show this as "Ctrl+C" appearing simultaneously, not "Ctrl press → C press → Ctrl release → C release". When you write the remap, this is what forces simultaneous with detect_key_down_uninterruptedly: true (default mandatory matcher misses single-report combos). 5. BT firmware modes can each emit different keycodes. When diagnosing a BT pad, repeat the diagnosis after switching modes (if the pad supports them). The Jieli/Free3-P has 4 modes — see `configure-macro-keyboard/references/08-bluetooth-configuration.md`.
Recent Changes
- No changes since 2026-04-21. The diagnostic workflow is stable. Updates to
configure-macro-keyboard(top-button tap/double-tap added 2026-04-24) did not require new diagnosis — the underlying button-to-keycode mappings did not change; only the rule that consumes them did.
Discoverability Notes
- This skill is the prerequisite for
configure-macro-keyboardwhen working with an unknown pad. If the user already knows what their buttons emit (e.g., they bought the same Jieli/Free3-P documented in this plugin), they can skip straight toconfigure-macro-keyboard. - TRIGGERS in
SKILL.mdcover both diagnosis-from-zero ("what does this button emit?") and diagnosis-on-failure ("my remap isn't firing — is the keycode what I think?").
HID Keycode Diagnostic Workflow — Expanded
The SKILL.md gives the essentials. This expansion covers three cases that trip people up:
1. Modifier + key in ONE HID report vs. sequential reports 2. Consumer keys (media/volume) that don't show in EventViewer's Main tab 3. Multi-interface devices where only one interface emits the button you care about
Case 1 — Telling Single-Report Combos from Sequential
Cheap pads emit Ctrl+C either as:
A. One HID report (report descriptor has the modifier byte set):
0x01 0x00 0x06 0x00 0x00 0x00 0x00 0x00
│ │ │
│ │ └─ keycode: C (0x06)
│ └─ reserved
└─ modifiers byte: left_control (0x01)B. Two sequential reports (first modifier, then key):
Report 1: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 (modifier alone)
Report 2: 0x01 0x00 0x06 0x00 0x00 0x00 0x00 0x00 (modifier + key)In Karabiner-EventViewer, both look similar but the microsecond timestamps differ:
- Single report:
left_controlandcshare the exact same timestamp (e.g.13:44:02.123456) - Sequential:
left_controlfires ~milliseconds beforec
Rule for writing the Karabiner matcher:
| Emission pattern | Karabiner matcher |
|---|---|
| Single report | "from": {"simultaneous": [{...ctrl}, {...c}], "simultaneous_options": {"detect_key_down_uninterruptedly": true}} |
| Sequential | "from": {"key_code": "c", "modifiers": {"mandatory": ["left_control"]}} |
If unsure, use `simultaneous` — it matches both patterns. The opposite is not true.
Case 2 — Consumer Keys Don't Show in Main Tab
If you press a button and nothing shows in EventViewer's Main tab, check the Unknown Events tab. Media keys (play/pause, volume, brightness) are emitted on HID Usage Page 0x0C (Consumer), not Usage Page 0x07 (Keyboard). Karabiner's Main tab only shows Keyboard page events.
When you find a consumer event, the matcher in Karabiner looks like:
"from": {"consumer_key_code": "volume_increment"}
"from": {"consumer_key_code": "volume_decrement"}
"from": {"consumer_key_code": "play_or_pause"}
"from": {"consumer_key_code": "scan_previous_track"}
"from": {"consumer_key_code": "scan_next_track"}The target uses the same namespace if you want to emit media keys: "to": [{"consumer_key_code": "..."}].
Case 3 — Multi-Interface Devices
A "USB Composite Device" like the Jieli 3-key pad typically exposes 2-4 USB interfaces (Keyboard HID, Consumer HID, System Control HID, Vendor HID). Each interface is a separate grabbable entity in Karabiner.
Symptom: your device_if rule matches but only some buttons trigger.
Diagnose:
# List all interfaces with their Karabiner device IDs
karabiner_cli --list-connected-devices | jq '.[] | select(.vendor_id == 19530)'Each output item is a separate interface. Look at is_keyboard, is_consumer, is_pointing_device, is_game_pad flags.
Fix: if your button emits on a non-keyboard interface, the default Karabiner device_if (which implicitly scopes to is_keyboard=true) won't match. Either:
- Add
is_consumer: trueto the identifier, or - Let Karabiner match multiple interfaces:
{ "vendor_id": 19530, "product_id": 16725 }(No is_keyboard constraint means it matches across interface types.)
The vk_none No-Op Trick
For a diagnostic rule that does nothing but forces Karabiner to grab the device so EventViewer can see events:
{
"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. The rule is inert but the device_if scoping forces Karabiner to attach to the device.
Checklist: Before Concluding "The Button Emits X"
- [ ] Checked Main tab (keyboard events)
- [ ] Checked Unknown Events tab (consumer events)
- [ ] Checked Devices tab — right VID/PID/interface?
- [ ] Pressed the button both briefly and held — some firmware emits different codes on repeat
- [ ] Unplugged & replugged the pad — some pads boot into a different mode after sleep/wake
- [ ] Tested on the _transport you plan to use_ (USB and BT emit different keycodes on the same pad)
- [ ] If pad has multiple firmware modes, mapped all modes (not just the current one)
Known Unknowns Worth Documenting
When you finish diagnosing a device, document in configure-macro-keyboard/references/:
1. VID/PID for USB and for BT (if BT-capable) 2. Number of USB interfaces and which one carries each button 3. Emission pattern per button (single-report vs sequential vs consumer) 4. Firmware modes and the button combos to switch between them 5. Anything that stops emitting after sleep/wake, unplug/replug, or firmware mode switch
This shortens future setup time from hours to minutes.