AI for Log Triage: Parsing macOS Unified Logs Without Losing Your Mind

Unified logging is how macOS records what it is doing, and most Mac admins still treat it as a wall they bounce off. You open Console, scroll for forty seconds, see nothing useful, and go back to guessing from the symptom. That is not a skills gap. It is a syntax problem — and syntax problems are exactly the kind of work worth handing to a model.
The distinction that matters: AI is very good at writing the filter and very bad at writing the conclusion. Keep those two jobs apart and log triage gets much faster. Blur them and you will paste a root-cause theory into your incident channel that a model invented.
The log is not a file, so grep is not an option
Apple is explicit about why this is harder than tailing a text log. The unified log "stores log messages in a binary compressed format, deferring much of the work to turn them into human-readable text messages," and because of that "you can't read and parse the log files directly; you need to use tools to convert the log messages." Apple names four readers: the Console app, the log command-line tool, Xcode's console, and the OSLog framework.
Volume is the second wall. Howard Oakley's write-up on unified log extraction is blunt: the log "can store tens of thousands of entries per minute," and when you are looking at unfiltered entries "anything more than a minute is going to be of limited value." An archive of the current logs runs around 700 MB, and the tracev3 files behind it "normally only cover the last few weeks."
So the real unit of work is not reading the log. It is writing a filter narrow enough that the log answers a question.
What log actually gives you
Four subcommands carry almost all of the admin workload, and it helps to have the exact shapes in front of you:
# shapes taken from the documented option list in man log(1)
sudo log show --last 1m
sudo log stream --debug --predicate 'subsystem=="com.apple.sharing" and category=="AirDrop"'
sudo log collect --output ~/Desktop/SystemLogs.logarchive --last 20m
sudo log show --archive ~/Desktop/SystemLogs.logarchive
sudo log config --status
--predicate is the whole game: the man page says it "Filters messages based on the provided predicate, based on NSPredicate." --last takes time[m|h|d] or the literal boot. Info-level messages are off unless you ask — "(By default info messages are not displayed.)" — and --debug turns on the noisier tier. --archive will "Display events stored in the given archive." log config requires root access, as do several of the others.
One asymmetry worth knowing before you build a pipeline: log show lists ndjson among its styles and log stream does not — stream's documented styles are default, compact, json and syslog. If you plan to pipe structured output anywhere, show is the subcommand that cooperates.
The field names, and the quoting that eats an afternoon
The man page documents nine predicate keys: eventType, eventMessage, messageType, process, processImagePath, sender, senderImagePath, subsystem and category. That list is the one to trust: you will find write-ups and forum replies claiming process is not a real field, while the man page defines it as "The name of the process the originated the event."
The quoting is where the afternoon goes. Predicates are NSPredicate, so Apple's predicate format reference governs the operators: CONTAINS, BEGINSWITH, ENDSWITH, LIKE — which allows ? and * as wildcards — and MATCHES, which is an ICU-style regex comparison. Logical operators can be spelled either way: AND or &&, OR or ||, NOT or !. And then the trap that silently returns nothing: "String comparisons are, by default, case and diacritic sensitive," which you fix with bracketed modifiers, [c] for case and [d] for diacritics. Kandji's admin guide says the same of log: comparisons are "case and accent (diacritic) sensitive by default."
Nest NSPredicate quoting inside shell quoting and you get the classic failure: the predicate was right and the shell ate the double quotes.
Where AI earns its keep
Approach | Where it wins | Where it falls over |
Console.app | Watching one Mac live while you reproduce the fault | Precision - you are scrolling, not querying |
log stream --predicate | Following one subsystem as the fault happens | The event has already happened, and there is no ndjson style |
log show --last, no predicate | Almost nothing | Tens of thousands of entries per minute |
log show --predicate | The actual job - a narrow question, a short answer | Recalling nine field names and NSPredicate quoting |
log collect, then --archive | The Mac is not in front of you | Around 700 MB per archive of current logs |
AI-drafted predicate | Symptom to filter in one pass, syntax included | Trusting the output instead of running it |
The pattern that works is narrow and boring. You describe the symptom in English — "MDM software update commands are failing on machines in the Dallas office" — and ask for a log show invocation and nothing else. You get back something close to Kandji's documented MDM example, process=="mdmclient" and subsystem=="com.apple.ManagedClient" and category=="OSUpdate", then narrow it with eventMessage contains once you know which string to look for.
Then you run it. That is the non-negotiable step, because the model's output is a query, not evidence. Everything after the run — which process failed, in what order, why — has to come from log lines you actually have on screen and paste back in. It is the same split that makes AI-assisted script review work: generate fast, verify against the real thing.
Two habits make it reliable. First, paste man log into the context before you ask, so the model writes against the flag list on the machine in front of you rather than its recollection. Second, make it name the field it chose. A predicate that filters on senderImagePath when you meant processImagePath returns zero rows, and zero rows looks exactly like "nothing happened."
The private-data wall
Eventually a model hands you a perfect predicate and the log hands you <private>. Apple masks a class of logged values by default; Kandji's guide describes the fix: a configuration profile, deliverable by MDM, that enables private data for one subsystem or system-wide. Hexnode's documented profile uses the payload type com.apple.system.logging with a System dictionary containing Enable-Private-Data set to true, and notes it "only works for devices running macOS 10.12 and later."
Do not deploy that fleet-wide. Kandji's own advice is to enable private data logging "only for specific subsystems, and only when absolutely necessary," and "to resume hiding private data, simply remove the configuration profile."
Know the ceiling before you spend a change window on it, too. On Apple's developer forums, Apple's Quinn explains that the setting only reveals data marked private, because there is a second tier above it: "the data is flagged as sensitive. Such data will never be recorded in the clear on production versions of macOS." A hash you get instead of a value is not something you can reverse or join across machines — Apple's Kevin Elliott notes that "the salt is generated randomly on a per-process basis, meaning that hashes cannot be correlated across processes or devices." It is a correlation token inside one process, and useless anywhere else.
If you let an agent run log show for you
Running the command yourself and letting an agent run it are different risk profiles, and Claude Code's permission rules repay a careful read before you choose the second.
Rules take the form Tool(specifier), and log is not in Claude Code's built-in read-only set — that set is ls, cat, echo, pwd, head, tail, grep, find, wc, which, diff, stat, du, cd and read-only forms of git — so a log show call needs approval unless you write a rule for it:
{
"permissions": {
"allow": [
"Bash(log show *)",
"Bash(log collect *)"
],
"deny": [
"Bash(log config *)"
]
}
}
Two documented behaviours matter here. The * goes after the subcommand, because Claude Code "matches everything before the first * as written" — Bash(log show *) allows only log show, while Bash(log *) would allow every log subcommand including config. And redirects are checked separately — "A rule such as Bash(git commit *) allows the command, not the target" — which is what you want when an agent is about to write a 700 MB archive. Deny wins outright, too: "An allow rule can't carve an exception out of a deny rule." If you want enforcement rather than configuration, that is what hooks are for.
At fleet scale
On one Mac this is a terminal habit. On ten thousand it is a collection problem, and Intune's shell-script surface draws the boundaries. Scripts need macOS 12.0 or later, the file "must be less than 1 MB in size," they run as root unless you set "Run script as signed-in user" to Yes, and scripts "running for longer than 60 minutes are stopped and reported as 'failed'." Status comes back as Success or Failed, nothing richer.
That tells you the shape of the answer: do not try to return log output through Intune. A custom attribute script's result "must be 20KB or less," and no useful log extract is that small. Have the script run log collect with a tight --predicate and --last, drop the archive somewhere you control, and open it later with log show --archive. Collect narrow at the edge, analyse centrally.
The practical takeaway
Use AI for the predicate. Use the log for the facts.
Concretely: keep a prompt that pastes in man log and asks for a single log show --last Nm --predicate '…' and nothing else. Run it. Feed the real output back before you let anything generate a theory. Reach for --info when the default level is too quiet, --style ndjson when you want to pipe it, and log collect --predicate plus --archive the moment the Mac is not in front of you. Leave Enable-Private-Data switched off until one specific subsystem forces the question, and expect it not to help if the value you want is flagged sensitive rather than private.
The reason to work in that order is not purity. A predicate is cheap to check — you run it, and either rows come back or they do not — while a root-cause narrative is expensive to check and easy to believe.
Sources: Apple — Viewing Log Messages, log(1) man page, Apple — Predicate Format String Syntax, Kandji — Mac Logging and the log Command, The Eclectic Light Company — macOS Unified log: content and extraction, Apple Developer Forums — Private data is still hidden with System-wide Enable-Private-Data, Hexnode — Configuration profile to enable private data in macOS Unified Logs, Microsoft Learn — Use shell scripts on macOS devices in Intune, Claude Code — Configure permissions



Comments