
Emit Fn Key On Macos
- 38 installs
- 62 repo stars
- Updated August 3, 2026
- terrylica/cc-skills
Helps with ai & agent building tasks.
About
emit-fn-key-on-macos is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- emit-fn-key-on-macos
- AI & Agent Building
- AI-coding skill
Emit Fn Key On Macos by the numbers
- 38 all-time installs (skills.sh)
- Ranked #8,450 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 emit-fn-key-on-macosAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 38 |
|---|---|
| repo stars | ★ 62 |
| Last updated | August 3, 2026 |
| Repository | terrylica/cc-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Emit a Real Fn Key on macOS
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.
Macos's Fn key (also called the "globe" key on modern MacBooks) is special. It's not a regular modifier — it carries kCGEventFlagMaskSecondaryFn, a flag that macOS's input subsystem discards if set from userland via `CGEventPost`. Many "remap anything" tools assume Fn behaves like Cmd / Option / Ctrl and try to emit it via CGEventPost — and silently fail for dictation, Typeless, macOS's dictation shortcut, Spotlight-via-globe, emoji picker, etc.
The only userland path that emits a Fn the kernel accepts is a DriverKit VirtualHIDDevice. Karabiner-Elements ships one. That's why Karabiner is the only FOSS tool for this job in 2026.
When to Use This Skill
- User wants a macro-pad button, mouse button, or any external key to trigger:
- Typeless push-to-talk (
pushToTalk: "Fn"by default) - macOS dictation (
Fn Fndouble-tap, or single-Fn in System Settings) - Emoji picker (
Fn+E) - Spotlight-via-globe (
Fn) - App-specific Fn combos that don't work when remapped by BTT
- User reports "BTT says the shortcut is firing but nothing happens in Typeless"
- User tries
hidutil property --setwith Fn mappings and sees no effect - User flashed QMK/ZMK on a board and Fn emission is broken
Prerequisite Check
# 1. Karabiner-Elements installed (the only userland tool that can emit real Fn)?
test -d /Applications/Karabiner-Elements.app && echo OK || brew install --cask karabiner-elements
# 2. Karabiner has Input Monitoring + Accessibility?
# System Settings → Privacy & Security → Input Monitoring → Karabiner = ON
# System Settings → Privacy & Security → Accessibility → Karabiner = ON
# 3. On macOS Sequoia+: privileged daemon approved in Login Items?
# System Settings → General → Login Items → Allow in the Background
# → Karabiner-Elements Privileged Daemon = ON
# Without this, Karabiner's DriverKit VirtualHIDDevice won't load and
# your Fn remap will silently do nothing.If Fn already works when pressed on your built-in keyboard but not via a remap, the DriverKit daemon is loaded but the remap rule is wrong — jump to "The One Magic Incantation" below. If even the built-in Fn stops working, re-check prerequisite 3.
The Core Truth
┌───────────────────────────┐ ┌───────────────────────────┐
│ Real Fn (from keyboard) │ │ Fn via CGEventPost(...) │
│ Hardware → DriverKit → │ │ Userland app → Quartz → │
│ HIDEvent with │ │ kCGEventFlagMaskSecon- │
│ NX_DEVICE_CAPABILITY_ │ │ daryFn flag is DROPPED │
│ INPUTKEYBOARD_FUNCTION │ │ before reaching input │
│ flag set │ │ subsystem. │
│ │ │ │
│ ✅ Typeless / dictation │ │ ❌ Typeless / dictation │
│ accept it. │ │ never fire. │
└───────────────────────────┘ └───────────────────────────┘Karabiner-Elements registers a DriverKit VirtualHIDDevice. When its remap rule says "emit Fn", it does so through the same kernel path a hardware keyboard uses. The NX_DEVICE_CAPABILITY_INPUTKEYBOARD_FUNCTION flag survives and Typeless/dictation accept it.
The One Magic Incantation
In a Karabiner manipulator's to block:
{ "apple_vendor_top_case_key_code": "keyboard_fn" }Not:
- ❌
{"key_code": "fn"}— no such key_code, silently ignored - ❌
{"modifiers": ["fn"]}— modifier flag without a key-down, doesn't trigger Fn semantics - ❌
{"consumer_key_code": "..."}— wrong HID usage page - ❌
{"key_code": "function"}— not a Karabiner keyword
The apple_vendor_top_case_key_code namespace is Karabiner's mapping for HID Usage Page 0x00FF (Apple Top Case) with Usage 0x03 (Keyboard Fn). That's the descriptor Apple's own internal keyboard uses.
Minimal Working Example
Remap the Caps Lock key (on a specific external keyboard only — adjust VID/PID) to emit real Fn:
{
"description": "Caps Lock → Fn (on external keyboard)",
"manipulators": [
{
"type": "basic",
"from": { "key_code": "caps_lock" },
"to": [{ "apple_vendor_top_case_key_code": "keyboard_fn" }],
"conditions": [
{
"type": "device_if",
"identifiers": [{ "vendor_id": 19530, "product_id": 16725 }]
}
]
}
]
}Drop into ~/.config/karabiner/karabiner.json → profile 0 → complex_modifications.rules.
Verification
# Open Karabiner-EventViewer
open -a "Karabiner-EventViewer"
# Press your remapped key; the Main tab should show:
# name: fn (not apple_vendor_top_case_key_code)
# ↑ Karabiner normalizes the display back to "fn" but under the hood it's emitting
# the Apple-Top-Case variant that carries the kernel flag.
# Then test the real consumer (e.g., Typeless with pushToTalk: "Fn"):
# Press and hold → mic opens
# Release → mic closes, transcript appearsIf EventViewer shows the right event but Typeless doesn't fire:
1. Check Typeless has Accessibility permission 2. Confirm pushToTalk in Typeless app-settings.json is set to "Fn" (not "fn", not "Function") 3. Confirm no other tool is also grabbing Fn — BTT with a Fn trigger will steal the event before Typeless sees it
Tap-vs-Hold: Do NOT Set Fn as a Held-Down Target
Attempted pattern: "tap top button → Return, hold top button → Fn" using Karabiner's to_if_alone + to_if_held_down.
This breaks Fn system-wide. Verified failure 2026-04-21: after loading a rule with to_if_held_down: [{"apple_vendor_top_case_key_code": "keyboard_fn"}], macOS's native Fn key stopped producing Fn even when pressed directly on the built-in keyboard. Full rollback required.
The reason is still unclear — possibly Karabiner's held-down state-machine holds the Fn-flag in a way that conflicts with real hardware Fn events. Until Karabiner upstream fixes it, use a separate button for Fn.
Why BTT Fails (One-Line Version)
BTT's "Trigger Key Sequence" and "Send Shortcut" actions go through CGEventPost with CGEventCreateKeyboardEvent + CGEventSetFlags. The flag kCGEventFlagMaskSecondaryFn is accepted by CGEventSetFlags (no error) but dropped by the input event manager before delivery. Apple has never documented this constraint; it's consistent across macOS 13 / 14 / 15.
Why hidutil Fails
hidutil property --set '{"UserKeyMapping":[...]}' operates at the HID report level and can swap keycodes, but it cannot synthesize the Fn _flag_. The flag is set by the keyboard driver (DriverKit or Apple's internal keyboard driver) based on which key was pressed — you can't forge it at the HID report level because it's a per-event computed property, not a persisted one.
Why QMK on Flashable Boards Can Work (but Jieli-Class Can't)
A QMK board with NKRO + Apple Fn keycode on an actual USB HID report descriptor that advertises Usage Page 0x00FF, Usage 0x03 can emit Fn natively. The kernel accepts it because it's a _real_ hardware report.
But cheap Jieli/Realtek/CH57x pads are not flashable — their firmware is burned in and exposes a fixed HID descriptor (usually standard keyboard Usage Page 0x07). No amount of reflashing with QMK/VIA/Vial works because the bootloader won't accept new firmware.
For flashable boards see: <https://github.com/qmk/qmk_firmware> → APPLE_FN_ENABLE.
Deep References
- `../configure-macro-keyboard/references/03-patterns.md` — "Apple vendor Fn encoding" pattern with full rule excerpt
- `../configure-macro-keyboard/references/04-anti-patterns.md` — BTT
CGEventPostfailure, tap-vs-hold Fn failure, QMK/VIA-on-Jieli failure - `./references/failed-approaches.md` — condensed failure catalog for this specific task
Sibling Skills
- `configure-macro-keyboard` — the end-to-end setup workflow that uses this skill as one step (wiring an external macro pad's button to Fn inside a larger Karabiner rule)
- `diagnose-hid-keycodes` — use this FIRST if you don't yet know which keycode your hardware emits; only then come back here to remap it to Fn
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.
emit-fn-key-on-macos Skill
Focused coverage of the one thing BetterTouchTool,hidutil, and QMK-on-locked-firmware all fail at: emitting a real Apple Fn key on macOS. Karabiner'sapple_vendor_top_case_key_code: keyboard_fn(HID Usage Page0x00FF, Usage0x03) is the only userland path because it routes through DriverKit VirtualHIDDevice with theNX_DEVICE_CAPABILITY_INPUTKEYBOARD_FUNCTIONcapability declared.
Hub: Plugin CLAUDE.md | Sibling skills: configure-macro-keyboard · diagnose-hid-keycodes
What This Skill Owns
| File | Role | Edit policy |
|---|---|---|
SKILL.md | Why-only-Karabiner explanation, with the canonical Karabiner snippet for emitting Fn. | Edit when a new Fn-emission method is verified to work (or to fail). Always test against Typeless pushToTalk: "Fn" AND macOS native dictation double-tap-Fn. |
references/failed-approaches.md | Condensed catalog of every Fn-emission attempt that didn't work. Append-only history. | Add new failures here. Don't remove entries — future readers need to know what's been tried. |
Critical Invariants
1. Real Fn requires the DriverKit VirtualHIDDevice capability declaration. App-layer synthetic events from CGEventPost (BTT, Hammerspoon, custom Swift apps) will never satisfy Typeless's CGEventTap filter or macOS dictation. This is not a bug — it's an OS-level safety mechanism around Fn / globe key behavior. 2. `key_code: fn` and `modifiers: ["fn"]` are no-ops in Karabiner. Only apple_vendor_top_case_key_code: keyboard_fn works. Don't paste examples from old Karabiner docs that suggest otherwise. 3. `to_if_held_down` with `keyboard_fn` breaks Fn system-wide. Verified failure (see references/failed-approaches.md). If you need tap-vs-hold discrimination on a button targeting Fn, use to_delayed_action (see configure-macro-keyboard → "Top-button caveat") or pick a different gesture. 4. The Globe key (`com.apple.HIToolbox AppleFnUsageType`) can hijack Fn presses. If a remap fires Fn but Typeless doesn't see it, check that defaults read com.apple.HIToolbox AppleFnUsageType returns 0 (Do Nothing). System Settings → Keyboard → "Press 🌐 key to..." controls this.
Recent Changes
- 2026-04-24 — A consumer of this skill (configure-macro-keyboard) shifted from immediate-Fn-on-press to delayed-Fn-via-
to_delayed_actionfor the MacroKeyBot top button. No changes to this skill. The Fn emission primitive is unchanged; only the trigger pattern in the consumer changed. The PTT-incompatibility caveat lives in the consumer's docs, not here.
Discoverability Notes
- This skill is intentionally narrow. If a user asks "how do I make my macro pad emit Fn?", they probably want `configure-macro-keyboard` (which uses this skill's primitive). This one is for the deeper "why doesn't BTT work?" question.
- The TRIGGERS frontmatter in
SKILL.mdis comprehensive (BTT failure symptoms, hidutil mapping not working, kCGEventFlagMaskSecondaryFn, etc.) — keep it broad to catch users arriving from search-based confusion.
Failed Approaches to Emitting Fn on macOS (2026)
Condensed catalog of everything that was tried before landing on apple_vendor_top_case_key_code: keyboard_fn via Karabiner-Elements. Every entry here was empirically tested against Typeless (pushToTalk: "Fn") on macOS 15 Sequoia, and macOS's native dictation double-tap-Fn.
BetterTouchTool (BTT)
Tried: BTT 4.x custom gesture → action Trigger Key Sequence with Fn as the key; also Send Shortcut with Fn as a modifier.
Result: BTT reports "action fired" in its log. Typeless mic does not open. macOS dictation does not activate.
Root cause: BTT internally calls CGEventCreateKeyboardEvent + CGEventSetFlags(..., kCGEventFlagMaskSecondaryFn) then CGEventPost. The kCGEventFlagMaskSecondaryFn flag is silently dropped by CGEventPost before the event reaches the input event manager. This is consistent across macOS 13/14/15 and undocumented.
Evidence: ran a CGEventTap that observed events at kCGHIDEventTap — BTT's Fn events showed up with the flag _missing_. A real hardware Fn press showed the flag _present_.
hidutil
Tried:
hidutil property --set '{"UserKeyMapping":[
{"HIDKeyboardModifierMappingSrc":0x70000003A,
"HIDKeyboardModifierMappingDst":0x700000065}
]}'(various source/dest combos attempting to remap a pad button to the Apple Top Case Fn usage)
Result: hidutil accepts the mapping, but the Fn flag is never synthesized by the OS for the remapped key.
Root cause: hidutil operates at the HID keycode level (swap keycode X for keycode Y in the device's report stream). The Fn _flag_ (NX_DEVICE_CAPABILITY_INPUTKEYBOARD_FUNCTION) is not a keycode — it's a per-event capability set by the keyboard driver based on which HID Usage Page / Usage fired. hidutil can't reach that layer.
QMK / VIA / Vial on Jieli-class Pads
Tried: connect the Jieli pad, run qmk list-keyboards + vial GUI to detect + flash.
Result: Neither tool sees the pad. Attempts to force it with --keymap fail at bootloader detection.
Root cause: Jieli AC69xx / similar cheap chipsets use a proprietary bootloader that only accepts vendor firmware. They are not QMK-compatible. No amount of dfu-util incantation will let you flash.
For pads that are flashable (ZMK-friendly, QMK-friendly), APPLE_FN_ENABLE=yes in the keymap's rules.mk + using KC_APPLE_FN emits real Fn. But you need a flashable board.
Python pynput / pyobjc CGEvent
Tried:
from Quartz import CGEventCreateKeyboardEvent, CGEventSetFlags, CGEventPost
# kCGEventFlagMaskSecondaryFn = 0x800000
ev = CGEventCreateKeyboardEvent(None, 63, True) # 63 = Fn virtual keycode
CGEventSetFlags(ev, 0x800000)
CGEventPost(0, ev)Result: Same as BTT — event posts, flag is dropped, Typeless does not fire.
Root cause: same mechanism as BTT. CGEventPost is the chokepoint.
AppleScript key code
Tried:
tell application "System Events" to key code 63Result: AppleScript errors — key code 63 is not a recognized virtual keycode in the AS bridge. Even if it worked, it would route through CGEventPost internally.
Hammerspoon hs.eventtap.keyStroke
Tried: hs.eventtap.keyStroke({"fn"}, nil) and variations.
Result: No effect — Hammerspoon uses the same CGEventPost path under the hood.
The One That Worked
Karabiner-Elements → remap rule with "to": [{"apple_vendor_top_case_key_code": "keyboard_fn"}].
Karabiner's DriverKit VirtualHIDDevice (org.pqrs.driverkit.VirtualHIDDevice-Manager) registers itself with the kernel as a real HID keyboard. Its emitted events carry NX_DEVICE_CAPABILITY_INPUTKEYBOARD_FUNCTION the same way an Apple internal keyboard does. Fn-flag survives. Typeless and macOS dictation accept it.
This is the only path in 2026. Monitor <https://github.com/pqrs-org/Karabiner-Elements> for changes, but the architectural constraint (flag set at driver level, not postable from userland) is unlikely to change.
Summary Table
| Tool / Approach | Why It Fails |
|---|---|
| BTT | CGEventPost drops kCGEventFlagMaskSecondaryFn |
hidutil | HID keycode layer can't synthesize the Fn capability flag |
| QMK/VIA/Vial on Jieli | Proprietary bootloader rejects non-vendor firmware |
Python pyobjc CGEventPost | Same as BTT — CGEventPost path |
AppleScript key code 63 | Not a valid AS keycode; also routes through CGEventPost |
Hammerspoon hs.eventtap | Same as BTT — CGEventPost path |
| Karabiner-Elements | Works — DriverKit VirtualHIDDevice emits at driver level |