# How Capslock works: F18, an event tap, and a mapping it gives back

> Caps Lock is a poor modifier as macOS delivers it. Capslock remaps it to F18, reads it from an active event tap, and changes only the part of the system key mapping it owns.

---

LLMS index: [llms.txt](/llms.txt)

---

Caps Lock sits in the best spot on the keyboard and does the least useful job.
Capslock gives it a new one: tap it for Escape, hold it for Hyper, and combine
it with the four modifiers to reach sixteen control planes. This post explains
how a native app does that on macOS without a kernel extension, a driver, or a
privileged helper.

## Caps Lock is not a modifier {#not-a-modifier}

macOS does not report Caps Lock as a key that goes down and comes back up. It
reports a lock state that flips, and it adds a short delay so that brushing the
key does not toggle it. That is fine for typing capitals and useless for a
modifier that must be held.

So Capslock starts with a remap. At activation it asks `hidutil` to send the
physical Caps Lock as **F18**, a key that no Apple keyboard has. F18 arrives as
an ordinary key-down and key-up pair, which the app can treat as "held" and
"released".

F18 has a second advantage: it is not one of the system modifiers. Holding
Caps and Shift and pressing H arrives as F18 held, Shift held, H. Nothing in
macOS combines F18 with Shift into something else, so all four modifiers stack
on top of Caps for free.

## Reading keys from an event tap {#event-tap}

Capslock installs a `CGEventTap` at the HID level, at the head of the queue, as
an active tap. Active means it can change or drop events, not just observe
them. macOS gates that on the **Accessibility** permission. It does not need
Input Monitoring, which only applies to listen-only taps.

If the permission is missing when the app starts, it keeps retrying. The tap
installs the moment you grant access, without a relaunch. The tap runs on its
own thread, and its callback does little more than compare integers, so typing
does not slow down.

## Resolving a chord {#chords}

When F18 goes down the app records that Caps is held and swallows the event.
When F18 comes back up it decides what the press was:

- **A tap**, with no other key pressed, runs the tap action. In the default
  profile that is Escape.
- **A chord**, Caps plus another key, runs the action mapped to that key on the
  current plane, and the original key never reaches the app in front.
- **A double tap**, when a mapping defines one, runs that instead. The single
  tap waits for the double-tap window so the two cannot both fire.

The plane comes from the modifiers you hold with Caps. Each modifier is a bit:
Command 1, Option 2, Control 4, Shift 8. Their sum names one of sixteen planes,
from 0 for Caps alone to 15 for all four. Matching is exact. A chord with an
extra modifier never falls back to a less specific plane, so adding Shift to a
navigation key can mean "select" without surprising you elsewhere.

Every event Capslock synthesizes carries a marker, and the tap skips events
that carry it. Without that, the arrow key Capslock sends for Caps+H would come
straight back through the tap.

## Two ways to be Hyper {#hyper-modes}

Earlier Capslock layouts implemented Hyper by holding the four right-side
modifiers. Launchers and window managers such as Alfred and Moom listen for
that combination, and many people have shortcuts built on it.

Capslock keeps this as a choice rather than an implementation detail:

- **Classic Compatibility** holds right Command, Option, Control and Shift for
  as long as Caps is down, so other apps see Hyper as before.
- **Native Clean** keeps Hyper private. Other apps see nothing while Caps is
  held, only the actions it produces.

In both modes the plane is computed from the modifiers you physically press.
The right-side keys Capslock holds for compatibility are excluded, or every
chord would land on plane 15.

## Owning only what it changed {#ownership}

`hidutil property --set` replaces the whole user key mapping. The obvious
implementation writes its own list on start and clears the property on quit,
which deletes any mapping another tool or the user had set. Capslock treats the
existing mapping as somebody else's:

1. It reads the current mapping and refuses to start if another entry already
   remaps Caps Lock.
2. Before changing anything it writes a recovery journal with the previous
   mapping and the single entry it is about to add.
3. It merges only its own entry and reads the result back to confirm it.
4. On Pause or Quit it removes only that entry and restores the previous
   mapping, unless something else changed it in the meantime.

If another program changes the mapping while Capslock runs, Capslock stops and
reports a conflict instead of fighting over the last write. It checks again
after sleep and wake, when a keyboard is plugged in, and when the user session
changes. This is also why Pause gives you a real Caps Lock back, not an F18
that is merely passed through.

## When the app does not quit cleanly {#crash}

A crash or a forced quit skips the clean-up in step 4. Two things cover that
case. A small watchdog embedded in the app reads the recovery journal and
watches its parent process; if the app exits without releasing the mapping, it
restores the previous one. The next launch also reconciles any journal it
finds before it installs the tap.

If you ever need to clear the mapping by hand, use the app's own command, which
removes only the Caps Lock entry it added:

```bash
/Applications/Capslock.app/Contents/MacOS/Capslock --restore-original-caps-lock
```

Do not reset the mapping with an empty list. That is global and would remove
every other tool's remapping too. The mapping does not survive a restart in
any case.

## What stays local {#local}

Everything above happens on your Mac. Capslock has no analytics or advertising
SDK and does not send keys, mappings, clipboard contents, or usage data
anywhere. Its diagnostic log leaves out typed text, clipboard contents, Shell
output, and app history. The [privacy page](/docs/reference/privacy/) lists
what is stored and when the app uses the network.

---

Backlinks:

- [Capslock goes native](/blog/post/capslock-goes-native/)
