A macOS daemon that turns a cheap Bluetooth smart ring into a wireless controller for AI agents and any app that accepts text input.
| Ring | VendorID | ProductID | Price | Where to buy |
|---|---|---|---|---|
| JX-11 | 0x05AC |
0x0220 |
~$15 | AliExpress, Amazon |
| JX-13 | 0x248A |
0x8251 |
~$15 | AliExpress, Amazon |
The bridge auto-detects which ring is connected. Adding a new ring is a single line in the code — see Adding a new ring below.
This lets you walk around your house, co-working space, or anywhere within Bluetooth range of your Mac and speak to your computer using just your voice and a smart ring on your finger.
Pair the ring with a wireless microphone (like a DJI wireless mic) and you can talk to AI agents like Claude Code and Codex, dictate notes, write emails, or put text into any application - all without being at your desk or keyboard.
The default ring controls:
| Ring action | Default mapping | Why you need it |
|---|---|---|
| Tap (press button) | Toggle voice recording (Right Option key) | Start and stop talking |
| Right arrow | Send (Enter key) | Submit what you said |
| Left arrow | Delete (Backspace key) | Remove what you just said if you misspoke |
| Scroll up/down (touchpad) | Scroll the screen | Read through output and code |
| Bottom button | Escape key | Interrupt the agent if it goes off track |
Note: The JX-13 only has a single button (no touchpad/swipe). It works perfectly for the core voice toggle workflow — tap to start, tap to stop.
All of these mappings are customisable - see Customising the controls below.
The ring connects to your Mac over Bluetooth and presents itself as a media controller (like headphone buttons). This daemon intercepts those signals before macOS turns them into volume/mute commands, and translates them into keyboard actions.
The voice recording toggle sends a quick tap of the Right Option key, which triggers toggle-mode voice recording in voice-to-text apps. This works with any voice-to-text app that supports keyboard shortcuts - for example FluidVoice, Superwhisper, Whisper Flow, Handy, and Every's Monologue. Just set your app's toggle shortcut to match the key the bridge sends (Right Option by default).
When you tap the ring, it starts recording. Tap again, it stops and your voice-to-text app transcribes and pastes the text.
- Smart ring: JX-11 or JX-13 Bluetooth ring (~$15 on AliExpress/Amazon)
- Voice-to-text app: Any app with a keyboard shortcut trigger (FluidVoice, Superwhisper, etc.)
- Computer: Mac running macOS (tested on macOS 15 Sequoia)
- Wireless mic (optional): DJI Mic, Rode Wireless Go, or any wireless microphone
You don't need a wireless mic. Your Mac's built-in microphone works fine when you're in the same room - the ring just lets you step back from your desk and not be tied to your keyboard. A wireless mic extends the range if you want to walk further away.
Put the ring in pairing mode (hold button until LED blinks rapidly) and connect it via Mac Bluetooth settings. It shows up as "JX-11" or "JX-13".
cd riff-bridge
make allThis compiles the Swift code and signs it with your Apple Developer certificate.
No Apple Developer certificate? That's fine — the binary still works unsigned for local use. You'll see a signing warning but it won't affect functionality.
The bridge needs two macOS permissions:
- Input Monitoring - to read the ring's button presses
- Accessibility - to send keyboard events to other apps
Go to System Settings > Privacy & Security and add the riff-bridge binary to both lists.
Set your voice app's toggle/push-to-talk shortcut to Right Option key:
- FluidVoice: Settings → Shortcut → press Right Option
- Superwhisper: Preferences → Keyboard Shortcut → Right Option
- Other apps: find the keyboard shortcut setting and assign Right Option
./riff-bridgeYou should see your ring detected and "Ring connected". Tap the ring — your voice app should start recording.
make installThis sets up a LaunchAgent so the bridge starts automatically when you log in and restarts if it crashes. No terminal window needed.
tail -f /tmp/riff-bridge.logTo stop: make uninstall
./riff-bridge # auto-detect connected ring
./riff-bridge --ring JX-13 # force a specific ring
./riff-bridge --list # show all supported rings- Put on the ring (and optionally clip on a wireless mic)
- Open any app you want to talk to - a coding agent, chat interface, notes app, email, anything
- Step back from your desk (or walk away if you have a wireless mic)
- Tap the ring to start recording your voice
- Speak naturally ("Create a new API endpoint for user profiles...")
- Tap again to stop recording - your voice-to-text app transcribes it
- (JX-11 only) Swipe right to send, swipe left to delete, scroll to read
- Single button only — no touchpad or swipe gestures
- Sends
volume_decrementas HID event (consumer key, usage0x00EA) - Sends duplicate events per tap — the bridge uses a 0.6s debounce to filter these
- Uses quick 50ms key tap (not hold) — works with toggle-mode voice apps like FluidVoice
- The ring may go to sleep after inactivity; press the button once to wake it before use
- If auto-detect fails after sleep, use
--ring JX-13to force selection
- Full touchpad with swipe gestures (left, right, scroll)
- Bottom button for Escape/interrupt
- See the original Riff repository for full JX-11 documentation
Ring not detected: Make sure it's paired in Bluetooth settings. Try --ring JX-13 (or JX-11) to force selection. Press the ring button to wake it from sleep.
FluidVoice not responding to ring: Check that FluidVoice's shortcut is set to Right Option. Test by physically pressing Right Option on your keyboard first — if that works but the ring doesn't, check Accessibility permissions.
Double-tap required: If you need to tap twice, the debounce may be too low for your ring. Edit kTapDebounce in riff_bridge.swift (default: 0.6s for JX-13).
Keys not working: Check Input Monitoring and Accessibility permissions in System Settings > Privacy & Security.
Bridge not starting: Run tail -f /tmp/riff-bridge.log to see error messages.
Scroll not working in terminal: Make sure tmux has mouse mode enabled (set -g mouse on in your .tmux.conf).
- Pair the ring via Bluetooth
- Find its IDs:
system_profiler SPBluetoothDataType | grep -A 5 "YourRing"
- Add one line to the
supportedRingsarray inriff_bridge.swift:RingDevice(name: "YourRing", vendorID: 0xXXXX, productID: 0xYYYY),
- Rebuild:
make all
If the ring sends duplicate events per tap (common with cheap BT rings), increase kTapDebounce (default: 0.6s).
| File | What it does |
|---|---|
riff_bridge.swift |
The main daemon code |
Makefile |
Build, sign, and install commands |
co.remotehumans.riff-bridge.plist |
LaunchAgent config (auto-start on login) |
Every gesture mapping in this bridge is customisable. The defaults (Right Option key for voice toggle, Enter to send, etc.) are just what worked for one particular setup - you can change any of them to match your workflow.
The mappings live in riff_bridge.swift and are straightforward to modify. If you use an AI coding agent like Claude Code, you can point it at this repo and ask it to change the mappings for you - for example, "change the tap gesture to trigger Cmd+Shift+A instead of Right Option" or "make the right swipe send Cmd+Enter instead of Enter". The code is intentionally simple so that AI agents (or you) can modify it easily.
Common customisations:
- Change the voice toggle key to match your voice-to-text app's shortcut
- Map swipe gestures to different keyboard shortcuts
- Add new gesture combinations for app-specific actions
- Adjust scroll speed and sensitivity
- Change
kTapDebounceif your ring double-fires
If you want to adapt this for a different smart ring or Bluetooth device, here's the process:
-
Intercept the raw signals - The ring shows up as a HID (Human Interface Device) over Bluetooth. We used macOS IOKit HID Manager to listen for any input from the ring and log every signal it sent - button presses, touch events, swipe directions, everything.
-
Map the signals - By pressing each button and gesture on the ring while logging (Karabiner-EventViewer is great for this), we built a map of which HID usage codes correspond to which physical actions.
-
Block the defaults - The ring's signals arrive as media keys (volume up, volume down, mute), so macOS tries to handle them. We set up a CGEvent tap to intercept these events and block the default behaviour before they reach the system.
-
Translate to useful keys - With the raw signals identified and defaults blocked, we mapped each ring gesture to the keyboard events we actually wanted (Option key tap, Enter, Backspace, scroll events, Escape).
-
Make it reliable - Set up a LaunchAgent so it auto-starts and restarts on crash, with a health check timer that reinstalls the event tap if macOS disables it.
- Written in Swift, no external dependencies
- Multi-device support via
RingDeviceregistry (auto-detection +--ringflag) - Uses IOKit HID Manager to read ring input (matched by VendorID/ProductID)
- Uses CGEvent tap to block default media key behaviour
- Timing correlation between IOKit and CGEvent callbacks prevents the ring's default volume/mute actions
- Quick key tap (50ms press+release) for toggle-mode voice apps
- 0.6s debounce for rings that send duplicate HID events
- Continuous scroll events during Y-axis swipes for smooth scrolling
- Runs as a macOS LaunchAgent with KeepAlive for automatic restart
Originally built by Remote Humans for the JX-11 ring. JX-13 multi-device support contributed by Morgan Schofield.
