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.