strafe holds macOS Accessibility permission and installs a system-wide event tap. That is a lot of trust to ask for, so this document states exactly what strafe can and cannot do, and how to verify every claim yourself. Every claim below points at a file and line you can read or a command you can run.
The whole program is about 1,486 lines of Swift + C (wc -l Sources/**).
You can build it from source in about 30 seconds (swift build) and audit it
in an afternoon.
strafe ships no binaries. It is distributed as source only — the only way to run it is to compile the code you can read. There is no prebuilt artifact, no download, and no update channel to trust. Updating means pulling this repository and building again.
This is also why the CI configuration holds no secrets. GitHub Actions
(.github/workflows/ci.yml) runs with permissions: contents: read, builds,
and verifies an ad-hoc bundle — there is no signing identity or publishing
credential anywhere in this repository to steal.
strafe installs one active CGEventTap and holds Accessibility permission to
do so. The tap's event mask is defined in exactly one place, and it covers
only gesture and dock-control events — not keystrokes.
-
Tap mask definition:
Sources/CStrafe/CStrafe.c, functionstrafe_tap_event_mask()(line 289):uint64_t strafe_tap_event_mask(void) { return (1ULL << kCGSEventGesture) | (1ULL << kCGSEventDockControl); }
That is
(1<<29) | (1<<30)— the two private trackpad-gesture event types and nothing else. There is nokCGEventKeyDown/kCGEventKeyUpbit. There is no second mask and no setting that widens this one. -
Why keys are excluded — determination comment: immediately above that function in
Sources/CStrafe/CStrafe.c(theKEY-EVENTS-IN-MASK DETERMINATIONblock, lines 267–288) documents that an earlier revision masked key events, that they were never acted on, and that they were removed. The tap now wakes only on real space-swipe gestures. -
The tap is installed here:
Sources/strafe/SwipeInterceptor.swift,SwipeInterceptor.start()(line 39; thetapCreatecall itself is at line 54), usingCGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: mask, ...)wheremaskcomes straight fromstrafe_tap_event_mask()above.
Because keystrokes are not in the mask, strafe cannot observe what you type.
A key event fails the cgsType == dockControl || cgsType == gesture guard
(SwipeInterceptor.handle, line 144) and is passed straight through, but in
practice a key event is never even delivered to the callback because it is not
in the tap's mask.
For the gesture events it does see, strafe reads a small fixed set of fields via
the wrappers in Sources/CStrafe/CStrafe.c (lines 221–242):
- CGS event type (field 55) —
strafe_event_cgs_type - IOHID gesture type (field 110) —
strafe_event_hid_type - swipe motion axis (field 123) —
strafe_event_swipe_motion - gesture phase (field 132) —
strafe_event_gesture_phase - swipe progress (field 124) —
strafe_event_swipe_progress - swipe velocity X (field 129) —
strafe_event_swipe_velocity_x - source process id —
strafe_event_source_pid
That is the entire surface of event data strafe inspects: enough to tell a real horizontal 3-finger space swipe from anything else, and its direction. No coordinates, no window contents, no clipboard, no key codes.
On macOS 27 and later, Sources/CStrafe/IOHIDPayload.c also serializes synthetic
events in memory to attach the raw IOHID payload required by the Dock (field
4205). Its position, phase, progress, and velocity values come from the event
strafe constructs, not recorded trackpad data. This code is adapted from
joshuarli/iss (0BSD; see LICENSE). It adds no permissions, input event types,
network access, or file access. The real swipe's terminal event is passed
through with its motion cleared after a replacement switch so the Dock can
finish its gesture state. At the first or last Space, strafe suppresses the
entire blocked swipe, including its terminal event, to prevent a bounce-back.
Beyond the swipe event itself, strafe also calls CGWindowListCopyWindowInfo
(reading window owner names and layer numbers, to detect whether Exposé/Mission
Control is open so it can pass real swipes through — strafe_is_expose_active,
Sources/CStrafe/CStrafe.c line 295) and reads the current cursor location to
pick which display to switch on (copy_cursor_display_identifier, same file
line 112). Neither the window list nor the cursor position is stored or
transmitted; both are read, used for that one decision, and discarded.
On macOS 27 and later, a Dock-owned window at layer 20 is enough to detect Mission Control; older systems keep the existing layer-18 requirement. This uses the same window metadata and adds no permissions or data collection.
Each of these is verifiable with a single grep over Sources/.
-
No network code, at all. strafe never opens a socket, makes an HTTP request, or resolves a host.
grep -rniE 'URLSession|NSURL|Network|CFSocket|socket|curl|http://|https://' Sources/Zero hits.
-
No third-party dependencies.
Package.swiftdeclares nodependenciesand no.package(...)entries — only Apple system frameworks (ApplicationServices,CoreFoundation,CoreGraphics,IOKit). Read the 35-linePackage.swiftin full. -
No analytics or telemetry. strafe writes only to the process's own
stderr(grep -rn FileHandle.standardError Sources/) andstdout(thestrafe statusCLI readout inPermissions.printStatus,Sources/strafe/Permissions.swiftline 28) — never to a network socket, a file, or an analytics sink. Nothing batches, serializes, or transmits usage. -
No auto-update, and no update check. strafe never downloads or executes anything. There is no updater, no Sparkle, no download URL, and nothing that asks a server whether a newer version exists (all covered by the network grep above). It cannot notify you of an update because it cannot reach the network at all; the menu bar just states the running version and where the source lives. Updating means pulling this repository and building again.
-
No dynamic loading. strafe does not
dlopen/dlsymanything. The private CGS symbols it uses are weak-imported at link time and guarded by an address check (strafe_cgs_available,CStrafe.cline 57):grep -rniE 'dlopen|dlsym' Sources/ # zero hits -
No file access. strafe opens no files. There are no
FileManager,contentsOfFile,fopen, or write calls inSources/(grep -rniE 'FileManager|contentsOfFile|fopen|write\(toFile' Sources/— zero hits). The one indirect exception isUserDefaults, which macOS backs with a plist — see the persistence bullet below. -
No subprocess execution. strafe spawns no processes. Unlike some prior art, it does not shell out to
tccutilor anything else (grep -rniE 'Process\(\)|/usr/bin|/bin/|tccutil' Sources/— no spawns). -
Persistence is limited to menu settings. strafe stores no databases and no caches. Its own code writes two
UserDefaultsvalues:transitionSpeed, an integer 0–2 recording which Transition speed preset you picked in the menu (TransitionSpeed,Sources/strafe/TransitionSpeed.swiftline 101); andspaceHotkeysEnabled, a bool recording whether the Ctrl+Option+Left/Right Space-switch hotkeys toggle is on (HotkeyManager,Sources/strafe/HotkeyManager.swift). Neither has any effect on what the gesture tap sees — the first changes the shape of the gesture strafe posts, the second only registers/unregisters a Carbon global hotkey (a separate mechanism from the tap, added so the hotkeys can be turned off independently if they conflict with a third-party shortcut bound to the same chord).Reads and writes go through one accessor, so the two launch modes (
strafe.appand the bare CLI, which has no bundle id) cannot land in different plists:grep -rn 'Preferences.store' Sources/ # two keys, plus cache synchronization grep -rn 'UserDefaults(' Sources/ # one hit: the suite in Preferences.swiftAppKit also saves menu-bar item visibility automatically when the icon is hidden or shown. strafe resets visibility on every fresh launch, so hiding the icon only lasts until the app is reopened or restarted.
Changing the hotkey setting flushes the shared preference and posts a local
DistributedNotificationCenternotification in the same login session. It carries no payload. A running strafe rereads its own preference and updates only its existing Carbon shortcut registrations; it does not accept commands or settings from notification data. This adds no network access or permissions.No usage data, no history, no coordinates are stored. Deleting
strafe.appleaves behind only that plist, whichdefaults delete com.rileycx.straferemoves (see README → Uninstall).
macOS only allows a process to create an active session-level event tap
(one that can suppress or modify events) if that process is trusted for
Accessibility. strafe's whole mechanism is to intercept your real 3-finger
space swipe, suppress the slow animated version, and post a faster synthetic
dock swipe in its place — that requires an active tap, which requires
Accessibility. (How much faster is the Transition speed setting; at every
preset it is the same event family, posted to the same tap, and it changes
nothing about what strafe can see.) See Sources/strafe/Permissions.swift for
the trust check (AXIsProcessTrusted / AXIsProcessTrustedWithOptions), which
is the only permission strafe requests.
strafe does not request Input Monitoring, Full Disk Access, Screen Recording, or any other permission.
git clone https://github.com/rileycx/strafe strafe && cd strafe
# 1. Build from source (~30s). No dependencies to resolve.
swift build
# 2. Count the codebase yourself.
wc -l Sources/strafe/*.swift Sources/CStrafe/CStrafe.c Sources/CStrafe/include/CStrafe.h
# 3. Confirm zero network / dynamic-loading / subprocess code.
grep -rniE 'URLSession|NSURL|Network|CFSocket|socket|curl|http://|https://|dlopen|dlsym' Sources/
grep -rniE 'Process\(\)|tccutil|/usr/bin|/bin/' Sources/
# 4. Confirm the tap mask excludes keystrokes, and that there is only one mask.
grep -rn 'strafe_tap_event_mask' Sources/
# 5. Confirm the one stored setting.
grep -rn 'Preferences.store' Sources/For the deep dive on exactly which private CGEvent fields are used and why, read
docs/SPEC.md. Caveat: docs/SPEC.md documents the upstream reference
implementation strafe was reimplemented from — the tccutil call, the second
event tap, and the key-event masking it describes are upstream-only and
intentionally absent from strafe.
Please report security issues through GitHub's private vulnerability reporting: open the repository's Security tab and choose Report a vulnerability. This keeps the report private until a fix is available. There is no email contact for security reports.