Blog Post Cover Image

Most macOS apps are black boxes. You click a button, something happens—maybe a network request, maybe some system interaction—but it's hard to know what unless you own the source code.

So I built a tool to watch.

This post walks through a hybrid SwiftUI + Rust project that logs every button click and tracks network traffic tied to the clicked app. It combines:

  • A SwiftUI frontend to simulate normal GUI interaction
  • A Rust backend using CGEventTap and the macOS Accessibility API
  • Per-process network inspection using the undocumented but powerful nettop

No kernel extensions. No root. Just clever usage of system APIs and a bit of FFI.

The full source code is available here.


Why build this?

Because reverse-engineering app behavior shouldn't require a debugger or Wireshark session. I wanted to:

  • Trace which UI elements trigger which network activity
  • Understand what apps do in response to input
  • Have a working reference for macOS Accessibility APIs in Rust

The Windows version of this tool relied on Win32 hooks and low-level TCP inspection via GetExtendedTcpTable.
You can read that walkthrough here →

This macOS build takes the same spirit to Apple's ecosystem—same goals, different APIs.


Demo: From Click to Packet

When a user clicks Button A, here's what gets logged:

📡 example-mac-app.47727 ↑ 6092 B ↓ 0 B (Δ ↑ 6092 ↓ 0)
[INFO] Button Clicked: App='example-mac-app', PID=47727, ID='ButtonA', Label='Button A'

You get:

  • The app name
  • The PID
  • The element's accessibility label and identifier
  • A delta of bytes sent/received from that process (captured via nettop)

Architecture Overview

+----------------------+       +-----------------------+
|   SwiftUI Mac App    | --->  |   Rust macos-watcher  |
| (3 Buttons + Network)|       | (Event Tap + nettop)  |
+----------------------+       +-----------------------+
              ⬑ Accessibility API (AXUIElement)
                        ⬐ CGEventTap mouse/keyboard

Frontend: SwiftUI Playground

The Xcode app has a simple interface with three buttons (ButtonA, ButtonB, ButtonC). When clicked, each makes a network request using URLSession.

Button("Button A") {
    fetchProduct(id: 1)
}
.accessibilityIdentifier("ButtonA")

The important part is setting .accessibilityIdentifier() so we can extract metadata from the Rust process using the Accessibility API.


Backend: Rust Input + Network Logger

This is where the real work happens.

🔹 Event Hooking

We use CGEventTap to hook into global input events like left-clicks and key presses.

let event_mask = (1 << K_CG_EVENT_LEFT_MOUSE_DOWN) | (1 << K_CG_EVENT_KEY_DOWN);
let event_tap = CGEventTapCreate(..., event_mask, event_callback, ...);

🔹 Accessibility API

On every click, we use AXUIElementCopyElementAtPosition to resolve which UI element is under the cursor. We extract:

  • PID of the owning app
  • Accessibility identifier (if set)
  • Role (e.g. AXButton)
  • Optional description or label
let result = ax_ui_element_copy_element_at_position(
    system_wide_element, x, y, &mut element_ref);

Then:

let pid = ax_ui_element_get_pid(element_ref);
let role = ax_ui_element_copy_attribute_value(element_ref, K_AX_ROLE_ATTRIBUTE);

🔹 Network Activity via nettop

After identifying the UI element and its app PID, we shell out to:

nettop -P -J bytes_in,bytes_out -x -l 1

We parse the output for lines matching the PID (e.g. example-mac-app.47727), and log any change in byte counts. This gives us a primitive—but real—view into per-process network activity.


Log Output Example

📡 example-mac-app.47727 ↑ 6092 B ↓ 0 B (Δ ↑ 6092 ↓ 0)
[INFO] Button Clicked: App='example-mac-app', PID=47727, ID='ButtonA', Label='Button A'

Each input event is logged with as much context as we can scrape. You can see which button triggered the traffic, and how much was sent/received.


Installation & Permissions

To run the watcher, build the Rust CLI and grant it accessibility permissions:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cd macos-watcher
cargo build --release
./target/release/macos-watcher

macOS will prompt you to allow the binary under:

System Settings → Privacy & Security → Accessibility

Once granted, you'll see logs at:

~/macos_watcher.log

You can also run it persistently in the background using the included .plist and install_daemon.sh.


Dev Notes

  • Built using core-foundation, objc, and simplelog
  • All UI inspection is done using C-level bindings to AXUIElement* APIs
  • There's no NSAccessibility or AppKit dependency in the Rust half
  • SwiftUI code is sandboxed and declarative. No extra work needed to simulate real-world interaction

Use Cases

  • Security - See if random apps are sending packets when buttons are clicked
  • Testing - Ensure your app only hits the network when expected
  • Debugging - Tie UI behavior to system-level effects
  • Education - Learn how accessibility and input work on macOS

Limitations

  • Requires GUI permission to run (can't be run headless)
  • nettop output is undocumented and may change
  • Packet contents aren't inspected—this is about attribution, not introspection
  • Only tracks visible, clickable elements (not gestures, background jobs, etc.)

Cross-Platform Parity

We now have both Windows and macOS versions of this tool, each using the platform's native APIs:

Feature Windows macOS
Input Hooking Win32 Low-Level Hooks CGEventTap
UI Element Metadata UIAutomation / IAccessible AXUIElement
Network Tracking GetExtendedTcpTable nettop
Language Rust Rust + SwiftUI

Future Work

  • Add packet inspection (via libpcap) for HTTP tracing
  • Tag UI actions with timestamps to correlate more precisely with network
  • Export events in JSON or send to external observability tools

Closing Thoughts

This project taught me a lot about how macOS input and accessibility work under the hood. The fact that you can hook into system-wide mouse events, pull out UI metadata, and match it to network traffic—all with a user-space Rust binary—is kind of wild.

If you want to analyze how apps behave without reverse-engineering them from scratch, this tool gives you a solid start.

Code is available here.