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
- Typeless push-to-talk (
- 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 appears
If EventViewer shows the right event but Typeless doesn't fire:
- Check Typeless has Accessibility permission
- Confirm
pushToTalkin Typelessapp-settings.jsonis set to"Fn"(not"fn", not"Function") - 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— BTTCGEventPostfailure, 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:
- Locate yourself. — Confirm this SKILL.md is the canonical file before any edit.
- What failed? — Fix the instruction that caused it.
- What worked better than expected? — Promote to recommended practice.
- What drifted? — Update vendor IDs, keycodes, or FOSS-tool versions if reality disagrees with the doc.
- Log it. — Add an evolution-log entry (or
04-anti-patterns.mdrow) with trigger, fix, evidence.
Do NOT defer. The next invocation inherits whatever you leave behind.