#801·robotjs

macOS: modifier keys posted as keyDown instead of flagsChanged (breaks VoiceOver chords)

Author: ravichaudhary8434Created Jul 15, 2026Updated Sep 14, 2026

Summary

On macOS, injected modifier keys (Control, Option/Alt, Command, Shift) are posted as keyDown/keyUp events instead of the flagsChanged events that real hardware emits. Assistive technologies that track modifier state from flagsChanged — notably VoiceOver — never observe an injected modifier as held, so modifier chords sent via RobotJS are ignored.

Details

In toggleKeyCode() (src/keypress.c):

c
CGEventRef keyEvent = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)code, down);
CGEventSetType(keyEvent, down ? kCGEventKeyDown : kCGEventKeyUp);   // <-- this
CGEventSetFlags(keyEvent, flags);
CGEventPost(kCGSessionEventTap, keyEvent);

CGEventCreateKeyboardEvent already creates the event with the correct type from its down argument, so the explicit CGEventSetType is redundant for normal keys. For modifier keycodes it is harmful: it forces them out as key-down/up rather than flags-changed.

Reproduce

  1. Run a CGEventTap listening for kCGEventFlagsChanged.
  2. robot.keyToggle('control', 'down').
  3. Observe the event arrives as keyDown (keycode 59), not flagsChanged. Real hardware, and a raw CGEventCreateKeyboardEvent posted without the CGEventSetType override, produce flagsChanged.

Practical impact: a VoiceOver command chord (Control+Option+key) injected via RobotJS is not recognised, because VoiceOver derives its VO-modifier state from flagsChanged.

Environment

  • macOS (reproduced on Apple Silicon and x86_64)
  • Present on master; the line is identical in 0.6.x–0.7.1

Fix

Remove the redundant CGEventSetType call. Normal keys are unaffected (their type is already set); modifier keys then post as flagsChanged. PR follows.

Note: verified behaviourally via an event-tap capture and against live VoiceOver; I was not able to run the project's own CI locally, so a maintainer check on non-accessibility key paths would be welcome.