How OpenLogi Captures Operating-System Input Using `openlogi-hook`: Architecture Deep Dive

OpenLogi captures mouse, scroll, and keyboard events through a unified Hook API that delegates to platform-specific backends—CGEventTap on macOS, evdev+uinput on Linux, and WH_MOUSE_LL/WH_KEYBOARD_LL hooks on Windows.

The openlogi-hook crate is OpenLogi's low-level input capture layer. It normalizes raw OS events into a common HookEvent type, allowing the agent system to react to user input regardless of platform. This article breaks down the architecture, key source files, and how each backend implements capture.


The Core Abstraction: Hook and HookBackend

All platform logic is hidden behind two types in crates/openlogi-hook/src/lib.rs.

Hook Struct

The Hook struct holds the running backend state as an opaque Option<Backend::Running>:

// crates/openlogi-hook/src/lib.rs#L14-L18
pub struct Hook<Backend: HookBackend> {
    running: Option<Backend::Running>,
}

This design lets Hook remain generic while each platform defines its own Running type—containing thread handles, run-loop references, or hook IDs.

HookBackend Trait

Every platform implements the HookBackend trait:

// crates/openlogi-hook/src/lib.rs#L31-L50
pub trait HookBackend: Sized {
    type Running;
    
    fn start<F>(callback: F) -> Result<Self::Running, HookError>
    where F: Fn(HookEvent) -> EventDisposition + Send + 'static;
    
    fn stop(running: Self::Running);
    fn is_running(running: &Self::Running) -> bool;
    
    // Platform-specific capabilities
    fn has_accessibility() -> bool;
    fn prompt_accessibility();
    fn list_event_taps() -> Vec<(u32, String)>;
    fn frontmost_app() -> Option<String>;
    fn cursor_position() -> (f64, f64);
}

The start method takes a callback closure that receives every HookEvent and returns an EventDisposition—PassThrough or Suppress—controlling whether the original event reaches other applications.


macOS Backend: CGEventTap and CFRunLoop

The macOS implementation in [crates/openlogi-hook/src/macos.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/macos.rs) uses Core Graphics event taps.

Accessibility Permission Check

Before capturing, the backend verifies the process has accessibility access:

// crates/openlogi-hook/src/macos.rs#L66-L80
fn has_accessibility() -> bool {
    // First check the standard API
    if !unsafe { AXIsProcessTrusted() } {
        return false;
    }
    
    // Also verify we can actually install a filtering tap
    probe_filtering_tap_available()
}

This dual check prevents false positives from AXIsProcessTrusted alone.

Starting the Hook

Hook::start delegates to the macOS backend, which:

  1. Spawns a watchdog thread to detect stalls
  2. Spawns a hook thread that creates a CGEventTap
// crates/openlogi-hook/src/macos.rs#L78-L98
let tap = CGEventTap::new(
    CGEventTapLocation::HID,           // Monitor system-wide HID events
    CGEventTapPlacement::HeadInsertFirst, // Intercept before other taps
    CGEventTapOptions::Default,
    hooked_event_types(),              // Mask of event types we want
    |_proxy, event_type, cg_event| {
        // Translate and dispatch...
    },
);

The hooked_event_types() function returns a mask including mouse button, scroll, move, and keyboard events.

Run-Loop Management

The tap is added to a CFRunLoop source, then executed in slices:

// crates/openlogi-hook/src/macos.rs#L108-L113
loop {
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.5, false);
    
    // Check if tap was disabled (accessibility revoked)
    if tap_is_disabled() {
        break;
    }
}

The 500ms timeout allows periodic checks for permission revocation while maintaining responsive event delivery.

Event Translation

Mouse events translate via translate; keyboard via translate_key:

// crates/openlogi-hook/src/macos.rs#L60-L74
fn translate_key(event: &CGEvent) -> Option<KeyEvent> {
    let keycode = event.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE) as u16;
    let pressed = event.get_type() == CGEventType::KeyDown;
    
    Some(KeyEvent {
        keycode,
        pressed,
        // ...modifiers, repeat flag
    })
}

The callback's return value determines event disposition:

// crates/openlogi-hook/src/macos.rs#L198-L206
let disposition = callback(event);
match disposition {
    EventDisposition::PassThrough => CallbackResult::PassThrough,
    EventDisposition::Suppress => CallbackResult::Suppress,
}

Linux Backend: evdev and uinput

The Linux implementation in [crates/openlogi-hook/src/linux.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/linux.rs) reads from kernel input devices directly.

Device Enumeration

The backend scans /dev/input/event* for devices supporting the needed event types:

// Conceptual flow from linux.rs
for path in glob("/dev/input/event*")? {
    let device = evdev::Device::open(&path)?;
    
    // Check if device supports keys, relative, or absolute events
    if device.supported_events().contains(EventType::KEY) 
        || device.supported_events().contains(EventType::REL) {
        devices.push(device);
    }
}

Event Reading Thread

Each device gets its own thread reading input_event structures:

// From linux.rs implementation
loop {
    match device.fetch_events()? {
        EventType::KEY => {
            let key_event = translate_key(&ev);
            let disposition = callback(HookEvent::Key(key_event));
            // Handle suppression...
        }
        EventType::REL => {
            let mouse_event = translate_relative(&ev);
            let disposition = callback(HookEvent::Mouse(mouse_event));
            // Handle suppression...
        }
        // EV_ABS for absolute positioning (touchpads, tablets)
    }
}

Pass-Through with uinput

When the callback returns PassThrough, events must still reach applications. The backend creates a uinput virtual device and re-injects the original event:

// Conceptual uinput flow
if disposition == EventDisposition::PassThrough {
    uinput_device.emit(input_event.type_, input_event.code, input_event.value)?;
}

This mirrors the original event stream while allowing the hook to intercept and optionally suppress.


Windows Backend: Low-Level Hooks

The Windows implementation in [crates/openlogi-hook/src/windows.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/windows.rs) uses Win32 low-level hooks.

Hook Registration

Separate hooks for mouse and keyboard:

// Conceptual from windows.rs
let mouse_hook = SetWindowsHookExW(
    WH_MOUSE_LL,
    Some(mouse_hook_proc),
    module_handle,
    0,
);

let keyboard_hook = SetWindowsHookExW(
    WH_KEYBOARD_LL,
    Some(keyboard_hook_proc),
    module_handle,
    0,
);

Message Loop Thread

Hooks require a thread with a message pump:

// From windows.rs implementation
loop {
    let mut msg: MSG = mem::zeroed();
    let result = GetMessageW(&mut msg, null_mut(), 0, 0);
    
    if result == -1 || should_exit {
        break;
    }
    TranslateMessage(&msg);
    DispatchMessageW(&msg);
}

Hook Procedures

The mouse_hook_proc translates MSLLHOOKSTRUCT to MouseEvent:

// Conceptual translation
unsafe extern "system" fn mouse_hook_proc(n_code: i32, w_param: WPARAM, l_param: LPARAM) -> LRESULT {
    if n_code >= 0 {
        let info = &*(l_param as *const MSLLHOOKSTRUCT);
        let event = translate_mouse(w_param, info);
        
        match CALLBACK.get()(event) {
            EventDisposition::Suppress => return 1, // Swallow event
            EventDisposition::PassThrough => {}     // Continue to CallNextHookEx
        }
    }
    CallNextHookEx(null_mut(), n_code, w_param, l_param)
}

Keyboard hooks follow the same pattern with KBDLLHOOKSTRUCT.


Practical Usage Example

use openlogi_hook::{Hook, HookEvent, MouseEvent, EventDisposition};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // macOS: ensure accessibility permission
    if !Hook::has_accessibility() {
        eprintln!("Please grant Accessibility permission");
        Hook::prompt_accessibility();
        return Ok(());
    }

    let hook = Hook::start(|event| {
        match event {
            HookEvent::Mouse(MouseEvent::Button { id, pressed, .. }) => {
                println!("Mouse button {:?} {}", id, if pressed { "pressed" } else { "released" });
            }
            HookEvent::Mouse(MouseEvent::Scroll { delta, .. }) => {
                println!("Scroll: {:?}", delta);
            }
            HookEvent::Key(k) => {
                println!("Key {}: code {}", if k.pressed { "down" } else { "up" }, k.keycode);
            }
        }
        EventDisposition::PassThrough // Allow event to continue
    })?;

    std::thread::park(); // Keep main thread alive
    
    // Cleanup on shutdown
    hook.stop();
    Ok(())
}

Watchdog Protection

All platforms use a watchdog in [crates/openlogi-hook/src/watchdog.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/watchdog.rs) to prevent system freezes. If the hook thread stalls—due to a blocking callback or deadlock—the watchdog aborts the process rather than leaving the system without input.


Summary

  • Hook provides a unified API across macOS, Linux, and Windows via the HookBackend trait abstraction
  • macOS uses CGEventTap with CFRunLoop, requiring explicit accessibility permission verification
  • Linux uses evdev for reading and uinput for pass-through, handling device enumeration per input node
  • Windows uses WH_MOUSE_LL/WH_KEYBOARD_LL low-level hooks with a dedicated message-pump thread
  • All platforms normalize events to HookEvent and respect EventDisposition for suppression control
  • A watchdog thread prevents system input hangs if the hook malfunctions

Frequently Asked Questions

What permissions does OpenLogi need for input capture?

On macOS, OpenLogi requires Accessibility permission (System Settings → Privacy & Security → Accessibility). The has_accessibility() method verifies this with both AXIsProcessTrusted() and a live tap probe. Linux requires read access to /dev/input/event* devices; Windows requires no special permissions beyond standard user execution.

Can OpenLogi suppress events so they don't reach other applications?

Yes. The callback passed to Hook::start returns an EventDisposition—either PassThrough or Suppress. On macOS this maps to CoreGraphics callback results; on Windows to CallNextHookEx or swallowing; on Linux to uinput re-injection or omission.

How does OpenLogi handle multiple input devices on Linux?

The Linux backend enumerates all /dev/input/event* devices supporting EV_KEY, EV_REL, or EV_ABS events. It spawns a dedicated thread per device, merging their event streams into the single user callback. This handles mixed setups like USB mice, laptop trackpads, and external keyboards simultaneously.

What happens if the hook callback blocks or crashes?

A watchdog thread in [watchdog.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-hook/src/watchdog.rs) monitors hook thread health. If the hook stalls beyond a timeout—due to a blocking callback or panic—the watchdog triggers a controlled process abort. This prevents the system-wide input freeze that would otherwise occur if a CGEventTap or low-level hook stopped processing events.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →