Summary
With an external keyboard attached, pressing that keyboard's Caps Lock is not reliably being intercepted by Karabiner for the Hammerspoon summon/macros flow.
The repo currently deploys a minimal ~/.config/karabiner/karabiner.json that maps caps_lock -> f13, but it does so by replacing Karabiner's full state file. That is brittle because Karabiner stores device/runtime state in the same file.
Expected
Any physical Caps Lock key, whether from the built-in Apple keyboard or an attached external keyboard, should be handled by Karabiner and produce the same F13 trigger used by Hammerspoon summon/macros.
Actual
Built-in keyboard flow is expected to work, but an attached external keyboard's Caps Lock is not reliably being "stolen" by Karabiner for the same F13 path.
Evidence
Repo-side Karabiner config is global, not device-scoped:
roles/hammerspoon/files/karabiner/karabiner.json contains a single complex modification mapping caps_lock to f13.
- There is no
device_if/device_unless condition in the checked-in rule.
The live Karabiner config currently matches the repo exactly:
~/.config/karabiner/karabiner.json contains only the same minimal profile/rule.
- It does not preserve a
devices array or other Karabiner-managed state.
Karabiner sees the external board:
{
"manufacturer": "Keychron",
"product": "Keychron Q5 HE",
"transport": "USB",
"device_identifiers": {
"is_game_pad": true,
"is_keyboard": true,
"is_pointing_device": true,
"product_id": 2896,
"vendor_id": 13364
}
}
Additional runtime checks:
karabiner_cli --show-current-profile-name => Default
karabiner_cli --version-number => 150900
karabiner_cli --list-connected-devices includes both the built-in Apple keyboard and the external Keychron Q5 HE
Why this repo is probably part of the problem
Even if the immediate external-keyboard failure turns out to be hardware-specific, this repo is managing Karabiner the wrong way.
roles/hammerspoon/tasks/MacOSX.yml currently copies a full karabiner.json into place:
roles/hammerspoon/tasks/MacOSX.yml
roles/hammerspoon/files/karabiner/karabiner.json
Karabiner's config file is stateful. Replacing it with a stub means we throw away any device-specific state Karabiner wants to maintain, which is exactly the wrong failure surface once multiple keyboards exist.
Proposed fix
Stop treating karabiner.json as a static dotfile.
Preferred direction:
- Stop copying a full
~/.config/karabiner/karabiner.json from this repo.
- Ship the CapsLock rule as an asset under
~/.config/karabiner/assets/complex_modifications/ instead.
- If we need the rule enabled automatically, patch/merge only the relevant
complex_modifications.rules entry rather than replacing the whole file.
- Validate with Karabiner-EventViewer on both:
- built-in Apple keyboard Caps Lock
- external
Keychron Q5 HE Caps Lock
- If the Keychron
Caps Lock event is genuinely different or unsupported, add a device-specific workaround or document the limitation.
Acceptance criteria
- Built-in Apple keyboard
Caps Lock triggers the Hammerspoon F13 summon path.
- External
Keychron Q5 HE Caps Lock triggers the same F13 summon path.
- Provisioning no longer overwrites Karabiner's full stateful
karabiner.json.
- Re-running
dotfiles -t hammerspoon does not erase Karabiner device state.
Summary
With an external keyboard attached, pressing that keyboard's
Caps Lockis not reliably being intercepted by Karabiner for the Hammerspoon summon/macros flow.The repo currently deploys a minimal
~/.config/karabiner/karabiner.jsonthat mapscaps_lock -> f13, but it does so by replacing Karabiner's full state file. That is brittle because Karabiner stores device/runtime state in the same file.Expected
Any physical
Caps Lockkey, whether from the built-in Apple keyboard or an attached external keyboard, should be handled by Karabiner and produce the sameF13trigger used by Hammerspoon summon/macros.Actual
Built-in keyboard flow is expected to work, but an attached external keyboard's
Caps Lockis not reliably being "stolen" by Karabiner for the sameF13path.Evidence
Repo-side Karabiner config is global, not device-scoped:
roles/hammerspoon/files/karabiner/karabiner.jsoncontains a single complex modification mappingcaps_locktof13.device_if/device_unlesscondition in the checked-in rule.The live Karabiner config currently matches the repo exactly:
~/.config/karabiner/karabiner.jsoncontains only the same minimal profile/rule.devicesarray or other Karabiner-managed state.Karabiner sees the external board:
{ "manufacturer": "Keychron", "product": "Keychron Q5 HE", "transport": "USB", "device_identifiers": { "is_game_pad": true, "is_keyboard": true, "is_pointing_device": true, "product_id": 2896, "vendor_id": 13364 } }Additional runtime checks:
karabiner_cli --show-current-profile-name=>Defaultkarabiner_cli --version-number=>150900karabiner_cli --list-connected-devicesincludes both the built-in Apple keyboard and the externalKeychron Q5 HEWhy this repo is probably part of the problem
Even if the immediate external-keyboard failure turns out to be hardware-specific, this repo is managing Karabiner the wrong way.
roles/hammerspoon/tasks/MacOSX.ymlcurrently copies a fullkarabiner.jsoninto place:roles/hammerspoon/tasks/MacOSX.ymlroles/hammerspoon/files/karabiner/karabiner.jsonKarabiner's config file is stateful. Replacing it with a stub means we throw away any device-specific state Karabiner wants to maintain, which is exactly the wrong failure surface once multiple keyboards exist.
Proposed fix
Stop treating
karabiner.jsonas a static dotfile.Preferred direction:
~/.config/karabiner/karabiner.jsonfrom this repo.~/.config/karabiner/assets/complex_modifications/instead.complex_modifications.rulesentry rather than replacing the whole file.Keychron Q5 HECaps LockCaps Lockevent is genuinely different or unsupported, add a device-specific workaround or document the limitation.Acceptance criteria
Caps Locktriggers the HammerspoonF13summon path.Keychron Q5 HECaps Locktriggers the sameF13summon path.karabiner.json.dotfiles -t hammerspoondoes not erase Karabiner device state.