macOS MCP audit logging: where macos-use already writes every tool call
Short version: it writes to /tmp/macos-use/. Every MCP tool call produces one .txt with the full pre- and post-action accessibility tree plus one .png of the target window. The path is also returned in the JSON-RPC response so the model that just called the tool always knows where the evidence landed. This is on by default; there is no flag to enable. The reference is Sources/MCPServer/main.swift:1961-1981.
Path: /tmp/macos-use/<unix-ms>_<tool>.txt and matching .png. Verified against Sources/MCPServer/main.swift lines 1961-1981. Writeout is atomic, survives crashes, and is mirrored to stderr. On the dev machine this page was written from, /tmp/macos-use/ currently holds 105 unique tool invocations across 5.7 MB.
What lands on disk per call
Four artifacts come out of a single CallTool. The .txt and .png are persistent on the local filesystem; the stderr line and the JSON-RPC reply are fire-and-forget. The reason there are four and not one is that the same data serves two different consumers: the model needs a path it can grep right now, and the operator needs an artifact that outlives the session.
<unix-ms>_<tool>.txt
Flat one-element-per-line dump of the post-action accessibility tree. Header gives app name, element count, processing time. Lines look like [AXButton (button)] "Send" x:734 y:512 w:64 h:32 visible. For mutating tools, the file also contains the + / - / ~ diff against the pre-action tree.
<unix-ms>_<tool>.png
PNG screenshot of the target window written by an out-of-process helper. The click point and window bounds are passed in, so the helper can mark exactly where the agent clicked.
stderr log line
Every writeout is mirrored to stderr with the full path and byte count: log: handler(CallTool): wrote full response to /tmp/macos-use/<ts>_<tool>.txt (12832 bytes). Useful when piping the server through a log aggregator that already eats stderr.
JSON-RPC response 'file:' line
The compact summary returned to the MCP client embeds the path as 'file: /tmp/macos-use/<ts>_<tool>.txt'. The model uses that path to grep for an element it wants instead of stuffing the whole tree into context. Audit trail and runtime data path are the same file.
Who writes what, and where it goes
Every MCP client points at the same Swift binary. The binary is the only writer. The accessibility tree, the screenshot, the stderr lines, and the JSON-RPC summary all originate inside one CallTool handler. There is no background daemon, no rotating log file, no buffer that can lose entries on a crash.
One handler, four outputs
The 20 lines that do the writeout
The whole audit-log mechanism is one block at the bottom of the CallTool handler. No abstraction, no logger framework, no rotating file appender. Plain String.write(toFile:atomically:encoding:) followed by a subprocess for the PNG.
Two details to notice. One: the timestamp is Unix epoch milliseconds, not seconds (Int(Date().timeIntervalSince1970 * 1000)). That is on purpose. The accessibility-tree walks are fast enough that two calls in the same second is normal; second-precision filenames would collide. Two: atomically: true means the write goes to a temp file and is renamed in place at the end. A crash mid-write leaves either no file or a complete file, never a half-written one. Your audit trail does not contain torn entries.
“On this dev machine, /tmp/macos-use/ currently holds 105 unique tool invocations across 128 files. The oldest entry is May 18, 2026; the newest is from a Slack-driving session four hours ago. None of those came from a flag I set; they were all written by default.”
ls -la /tmp/macos-use/ | wc -l, du -sh /tmp/macos-use/, 2026-05-20
The lifecycle of one audit entry
End-to-end, here is what happens between "Claude Code decided to click" and "there is a .txt and a .png on disk." Reading this top to bottom is also how you would read the stderr log of a live server.
From CallTool to /tmp/macos-use/
Tool call arrives
Claude Code (or Cursor, Cline, Claude Desktop) sends a CallTool JSON-RPC over stdio. main.swift:1549 logs the tool name; main.swift:1550 logs the raw arguments to stderr.
Pre-action traversal
If traverseBefore is set, the server walks the target app's AX tree first, so the diff has a baseline. The pre-action tree never hits disk on its own; it is differenced against the post-action tree.
Primary action runs
Click, type, scroll, set_value, press_ax, or set_selected. CGEvent for the synthetic-input variants, AXUIElement for the AX-attribute variants.
Post-action traversal + flat-text build
Tree is walked again, diffed, flattened by buildFlatTextResponse. resultTextString now holds the full payload, typically 2 KB to 60 KB.
.txt write
atomicall write of <unix-ms>_<tool>.txt to /tmp/macos-use/. Atomic-rename semantics mean a crash leaves either the complete file or no file, never a torn one.
.png capture
captureWindowScreenshot forks the screenshot-helper subprocess. PNG lands at <unix-ms>_<tool>.png alongside the .txt.
Compact summary returned
buildCompactSummary builds the response: status, pid, app, file:, file_size, hint, screenshot:. The model reads that and decides the next step.
Keeping the trail past macOS's /tmp cleanup
macOS clears /tmp/ aggressively. The periodic cleaner at /etc/periodic/daily/110.clean-tmps deletes files older than 3 days; a reboot wipes the rest. If you actually want a retained audit trail you need to copy out. The smallest workable setup is one rsync and one gzip, both wired to launchd.
The .txt files compress to roughly 10-20% of their on-disk size because the AX tree is repetitive (every row of a list has the same role and column structure). The .png screenshots are already PNG-compressed and do not shrink much further; if disk is the constraint, the cheapest move is to drop the .png and keep only the .txt. The text-only audit still contains every visible label and typed-in value, just not the rendered pixels.
What the trail does not capture (yet)
Two specific gaps worth knowing before you sell this as a complete audit log to a security review.
- Raw request arguments are stderr-only. The line
log: handler(CallTool): arguments received (raw MCP): ...(main.swift:1550) prints what the agent asked for, but that never makes it into the .txt file. If Claude Code is redirecting stderr to /dev/null (it sometimes does, depending on the launch path), you lose the "what was the model trying to do" half of the audit. The fix is a 10-line change: prepend a header toresultTextStringbefore the write, dumpingparams.arguments?.debugDescription. - Cancelled calls leave no file. When the user presses Esc, InputGuard fires and the handler returns at main.swift:1988 without writing anything. You see the cancellation on stderr but you cannot reconstruct what the agent was about to do from /tmp/macos-use/. For a tamper-evident trail, you want a .txt for the cancelled action too, marked as such. That is another 10-line change in the catch arm.
Neither of these is hard, but neither is shipped today. Treat the current trail as a great development and debugging artifact, and a 80% audit log. The remaining 20% is one PR away.
macos-use vs auditd vs the MCP client log: three audit layers
If you searched and landed on Apple's auditd / /var/audit docs first, that is a different layer, and on a modern Mac it is probably not even running. There are really three places an AI agent driving macOS can leave a trail, and they barely overlap. Here is who captures what.
| /tmp/macos-use/ trail | macOS BSM auditd | MCP client session log | |
|---|---|---|---|
| Layer it sees | Application UI — the AX element acted on | Kernel — syscalls, file opens, execs, logins | Conversation — model turns and tool calls |
| What the agent clicked / typed | Yes, with the before/after AX diff | No | No — only the model's intent, not the result |
| The model's request + tool arguments | Stderr only, not folded into the .txt | No | Yes — full prompt and tool args |
| Filesystem / process events | No | Yes | No |
| Screenshot of the window | Yes, one .png per call | No | No |
| On by default (macOS 14 Sonoma+) | Yes | No — deprecated since 11.0, disabled since 14.0 | Yes — the client writes it |
| Survives a client crash | Yes — atomic write + rename | Yes — kernel-owned | Partial — depends on flush timing |
| Where it lives | /tmp/macos-use/ | /var/audit/ | ~/.claude/projects/…/*.jsonl |
The catch with "just turn on auditd": Apple deprecated the BSM audit subsystem in macOS 11, disabled it by default in macOS 14 (Sonoma), and has flagged it for removal. On the exact OS versions macos-use targets (macOS 13+), the kernel-layer audit you are picturing is most likely off until someone re-enables a deprecated service and reboots. The /tmp/macos-use/ trail, by contrast, is on by default with no flag.
For a complete picture you would correlate all three. auditd answers "did anything weird touch the filesystem during that session?". The MCP client log answers "what did the model ask the agent to do?". /tmp/macos-use/ answers "what did the agent actually click, type, and scroll, and what did the screen look like after?". Whichever question is keeping you up at night is the one to wire up first.
Wiring this trail into a SIEM or a security review?
If you are forking the server to harden the audit log (session IDs, encrypted-at-rest, redaction), 20 minutes with the maintainer beats another evening reading main.swift.
Frequently asked
Frequently asked questions
Where does the macos-use MCP server write its audit log by default?
To /tmp/macos-use/ on the host that runs the server. Every CallTool handler creates that directory with FileManager.default.createDirectory at Sources/MCPServer/main.swift:1962, then writes <unix-ms>_<safe-tool-name>.txt with the full flat accessibility tree, plus <unix-ms>_<safe-tool-name>.png with a window screenshot. The path is also returned in the JSON-RPC response as the 'file:' line, so the model that just called the tool always knows exactly where the evidence landed. No env var, no flag, no opt-in. It is on by default.
What exactly is in each .txt file?
A flat one-element-per-line dump of the post-action accessibility tree (and the pre/post diff for click, type, scroll, press). Each line is '[AXRole (role description)] "text" x:N y:N w:W h:H visible', plus a header with the app name, element count, and processing time. The format is built by buildFlatTextResponse and formatElementLine in main.swift. The diff sections are tagged with + for added nodes, - for removed, and ~ for modified attributes (main.swift:1029-1047), so you can reconstruct what the agent changed about the UI without replaying anything.
What is in each .png file?
A PNG of the target window captured by the screenshot-helper subprocess immediately after the action runs. captureWindowScreenshot in main.swift:389-510 picks the on-screen window with the best score for the PID the tool acted on, then forks an out-of-process helper to write the PNG to /tmp/macos-use/<unix-ms>_<tool>.png. The click point and window bounds are passed in, so a red dot can be drawn on what the agent clicked. If the screenshot helper is missing or times out, the .txt still gets written; the audit trail degrades gracefully to text-only rather than failing the call.
Does the audit log survive a Claude Code or Cursor crash mid-session?
Yes. The writeout happens inside the CallTool handler before the response is returned (main.swift:1968 'try? resultTextString.write(toFile: filepath, atomically: true, encoding: .utf8)'). Atomically means the file is written to a temp path and renamed in place, so a crash mid-write leaves either the complete file or no file, never a torn one. If the MCP client crashes after the tool finished but before it read the response, /tmp/macos-use/ still has the .txt and .png. You can replay the session by ls-ing the directory in timestamp order.
How do I keep the audit trail instead of letting macOS clear /tmp/?
macOS clears /tmp/ on reboot via launchd and the periodic cleaner in /etc/periodic/daily/110.clean-tmps, which deletes files older than 3 days (the +mtime +3 rule in that script). To keep your trail, run a one-line rsync on a launchd interval: 'rsync -a /tmp/macos-use/ ~/Library/Logs/macos-use/'. The .txt files compress to roughly 10-20% of their on-disk size with gzip because the accessibility tree is repetitive. A second cron job that does 'find ~/Library/Logs/macos-use -name '*.txt' -mtime +1 -exec gzip {} +' will keep the long tail readable without blowing through disk. For SIEM ingestion, tail the directory with fswatch and pipe new files into your collector.
How do I correlate an entry in /tmp/macos-use/ back to a specific Claude Code conversation?
The filename timestamp is Unix epoch milliseconds (Int(Date().timeIntervalSince1970 * 1000), main.swift:1964). Cross-reference it against Claude Code's session log under ~/.claude/projects/<encoded-path>/sessions/<uuid>.jsonl, which records every assistant turn with its start time. For Cursor, the equivalent is the chat history file in the workspace state. The MCP server itself does not carry a session ID; correlation lives at the client. If you need first-class session attribution, fork the server and add a session header to the env (Claude Code passes env vars through to the spawned MCP process).
What is NOT captured in the current audit log?
Two specific gaps. (1) The raw MCP request arguments are logged to stderr ('log: handler(CallTool): arguments received (raw MCP): ...' at main.swift:1550) but are not folded into the .txt file. If your stderr is going to /dev/null because Claude Code suppresses it, you lose the 'what did the agent ask for' half of the audit. (2) Cancelled tool calls (the user pressed Esc, InputGuard fires) emit a 'log: handler(CallTool): user cancelled' line to stderr (main.swift:1988) but do not write a .txt for the cancelled action; only completed and errored calls leave a file. Both gaps are 20-line patches if you need a tamper-evident, complete trail.
Is /tmp/macos-use/ writable by other users on the same Mac?
No, the directory is created with default permissions (drwxr-xr-x, owned by the user that ran the MCP server). Other local users can read the files but cannot write or modify them. If you want to harden this further, move the output dir to ~/Library/Logs/macos-use/ in your fork (it is a one-line change at main.swift:1961) and the directory inherits ~/Library/Logs/ permissions, which are owner-only. The host process needs Accessibility permission to capture the AX tree at all, so a non-admin user on the machine cannot scrape a trail from another user's MCP session without already having sudo.
Does this audit log capture screenshots of windows that contain sensitive data?
Yes, by default, every tool call that succeeds against a window produces a .png of that window. If you are driving 1Password, Mail, Slack DMs, or a banking site, the rendered pixels of that window end up on disk in /tmp/macos-use/. If that is not acceptable for your environment, pass an env var check around the captureWindowScreenshot block at main.swift:1977-1979 in your fork ('if ProcessInfo.processInfo.environment["MACOS_USE_NO_SCREENSHOT"] == nil { ... }'). The .txt accessibility tree will still contain element text (typed-in fields, visible labels), so a text-only audit is not the same as no audit.
How does this compare to using the macOS BSM auditd (the /var/audit log)?
Different layers entirely. auditd captures kernel-level events: file opens, process execs, syscalls, login attempts. It does not know what an MCP tool call is, and it does not see what the model asked the agent to do. The /tmp/macos-use/ trail captures the application layer: which AX element was clicked, what the window looked like after, what changed in the tree. One practical wrinkle: Apple deprecated the BSM audit subsystem in macOS 11 and disabled it by default in macOS 14 (Sonoma), with removal flagged for a future release, so on the OS versions macos-use targets (macOS 13+) auditd is most likely off until you re-enable a deprecated service and reboot. The /tmp/macos-use/ trail is on by default with no flag. For a complete picture you would correlate both: auditd answers 'did anything weird touch the filesystem?'; /tmp/macos-use/ answers 'what did the AI agent click?'. The two never overlap and the second is not redundant with the first.
More on what macos-use actually writes, reads, and changes
Keep reading
MCP write tools vs read: when to split, when to fuse
Why every macos-use action fuses write + read into one call, and what that means for the diff you see in each audit entry.
MCP server discovery limits on macOS
What discovery captures (everything) versus what actuation can drop silently (Catalyst right-pane controls). Reading the diff in an audit entry is how you spot the dropped clicks.
Drive native macOS apps via the AX tree with MCP
The 9 tools, with examples of what each writes to /tmp/macos-use/. Where the audit trail starts.
Comments (••)
Leave a comment to see what others are saying.Public and anonymous. No signup.