The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Crashdx listing page.
A crash diagnosis engine for Apple platforms: parses .ips crash reports, symbolicates
them against dSYMs, and produces an evidence-cited, ranked diagnosis, not just a
symbolicated stack trace.
Ships as a dependency-free Swift library (CrashDXCore imports only Foundation), a CLI
(crashdx), and an MCP server (crashdx-mcp), so agents and humans use the same engine.
A symbolicated stack trace tells you where a process died, not why. crashdx adds a second
stage on top of symbolication: deterministic, rule-based evidence extraction (watchdog
budgets, jetsam tables, register and memory state, lastExceptionBacktrace/asi) feeding
a ranked set of competing hypotheses, each citing the specific facts that support it and
pointing back into the raw report.
Two properties it holds onto deliberately:
There are no LLM calls in the engine: it produces verifiable facts and ranked interpretations, and leaves the narrative to whatever consumes them. See docs/DESIGN.md for the full architecture.
Everything below is real, unedited output from fixtures in this repo (elisions are marked).
Paths are relative to the repo root, and the --dsym flags point at dSYMs the repo ships,
so these reproduce as-is; use swift run crashdx ... if you have not installed the binary.
The evidence line cites the supporting facts the rule scored on, up to four, with a
running total when there are more; each fact carries a path back into the original report,
which --json --tier standard exposes. also considered is the full remainder of the
ranked list, so you can see what the verdict beat and by how much.
When the leading hypothesis is not strongly supported and clearly ahead, you get the candidates instead of a label:
The same shape applies across the rule set. Verdict and evidence lines from four more fixtures, with the explanation and follow-up steps elided:
Each of those also prints the full explanation, inspect point, and confirm steps shown
in the first example.
--json emits the same analysis as a structured AnalysisReport, which is what the MCP
server returns and what you would parse in CI. Below is the same crash as the first
example, abridged (/* ... */ marks elisions). The real output is a single minified line
with sorted keys, so pipe it through jq or python3 -m json.tool; it is pretty-printed
and reordered here for readability:
Note "leb": true on the inspect point: the throw site was recovered from
lastExceptionBacktrace rather than the faulting thread, and the report says so instead of
flattening the distinction.
--tier standard (see Report tiers) adds diagnosis.factsConsidered,
which resolves every factID to its human-readable statement and its sourcePath into the
raw report:
sourcePath is not optional on a Fact, so every fact the diagnosis cites can be walked
back to the part of the report it was read from.
Committed golden snapshots of both
tiers live in Tests/CrashDXCoreTests/Fixtures/ (nsexcrash-summary-golden.json,
nullderef-standard-golden.json) if you want the complete, unabridged shape.
swift-tools-version:6.0, but the MCP
server's pinned dependencies declare up to 6.2, and SwiftPM resolves the whole
package graph, so 6.2 is the real floor for every target, including CrashDXCore.CrashSymbolicator.py from Xcode's CoreSymbolicationDT.framework, located via
xcode-select -p. Without it, crashdx falls back to atos, which resolves fewer
source locations. Parsing and diagnosis work either way./usr/bin/python3 and /usr/bin/dwarfdump (both provided by Xcode's command line
tools), used to run CrashSymbolicator.py and to verify dSYM UUIDs.Honest scope, because a crash diagnoser that quietly does worse on your platform is worse than one that says so:
x86_THREAD_STATE registers and the 4 KB page
size. There is no Intel fixture in the corpus, so this path is reasoned-about rather
than exercised against a real report.bug_type 309/109). Jetsam event reports (JetsamEvent-*.ips,
bug_type 298) and hang/stackshot reports (bug_type 288) use a different payload
shape with no threads or exception object; crashdx parses them without error but has
no facts to work from and will report inconclusive.crashdx runs entirely on your machine and makes no network calls of any kind. Crash
reports, dSYMs, and everything derived from them stay local. This matters because .ips
files contain identifying data (crashReporterKey, boot/sleep-wake UUIDs, device model,
and usernames in paths), so if you attach one to a bug report, scrub it first.
The binaries land in .build/release/. To put them on your PATH:
(Or copy them somewhere you already own, such as ~/.local/bin.)
Crash reports live in ~/Library/Logs/DiagnosticReports/ on macOS (or Console.app →
Crash Reports); on iOS, Settings → Privacy & Security → Analytics & Improvements →
Analytics Data, and Xcode's Organizer for TestFlight/App Store crashes.
--dsym accepts a .dSYM bundle, an .xcarchive, or a directory to search recursively
(repeatable), and --no-spotlight skips Spotlight-based discovery while --no-archives
skips Xcode's archive directory. Run crashdx <subcommand> --help for the full option
list. Note that --json and --tier apply to analyze only; symbolicate always emits
a complete .ips.
Both subcommands search Spotlight and Xcode's archives for a matching dSYM automatically.
For foreign reports (user-submitted, TestFlight, CI artifacts) pass the build's dSYM
explicitly with --dsym.
Strings taken from the report (process name, symbols, paths, exception text) are escaped
before the human-readable summary prints them: C0 controls, DEL, and bidi overrides
render as \x0A / \u{202E} instead of being emitted, so a crafted report cannot forge
a DIAGNOSIS: line or repaint your terminal. --json and symbolicate emit the
report's strings verbatim; treat their output as data, not as terminal text.
A dSYM from a different build is refused rather than used: a stale dSYM produces
confidently wrong symbols, which is worse than none. That case is reported as
uuid_mismatch (with the offending path in reason), kept distinct from no_dsym
because the fix is different: find the archive matching the report's build UUID, rather
than go looking for a file you may already have.
--tier controls how much of the report is emitted; it never changes the diagnosis,
which is always computed from the full report:
| Tier | Contents |
|---|---|
summary (default) | Faulting thread (capped at 15 frames), lastExceptionBacktrace, and the diagnosis verdict + ranked hypotheses |
standard | Lifts the frame cap, and adds the binary images, other threads that contain app frames, and factsConsidered (the evidence behind each hypothesis) |
full | Adds every remaining thread, including those with no app frames |
0 success · 64 usage error · 65 the input could not be parsed as an .ips report,
or symbolication failed · 66 file not found or unreadable.
Diagnostics go to stderr; stdout carries report output (and --help/--version).
crashdx-mcp exposes the same pipeline over stdio (newline-delimited JSON-RPC) as the
crashdx_analyze and crashdx_symbolicate tools. With Claude Code:
A matching crash-triage Agent Skill ships in .claude/skills/crash-triage/, covering
how to read the diagnosis and the interpretation pitfalls specific to each crash family.
Add https://github.com/r00tify/crashdx.git to your Package.swift, depend on the
CrashDXCore product, and call
AnalyzePipeline.analyze(path:tier:dsymPaths:useSpotlight:searchArchives:), the same
entry point both binaries use.
CrashDXCore imports nothing but Foundation, but two limitations are worth knowing
before you depend on it:
atos/dwarfdump/mdfind through
Foundation.Process, which does not exist on iOS. Adding this package to an iOS target
fails during dependency resolution, before anything compiles.Package.resolved even if you never import them.AnalysisReport and the diagnosis model are Sendable, so analysing a directory of
reports concurrently works as you would expect.
See CONTRIBUTING.md. One rule matters more than the rest: crash
reports and dSYMs must be scrubbed before they are committed. They carry device
identifiers and your username. Scripts/scrub-fixture.py handles .ips files and
Scripts/check-fixtures-scrubbed.sh (also run in CI) will tell you if anything slipped
through.
crashdx makes no network calls; nothing leaves your machine. See SECURITY.md
for vulnerability reporting and for what a .ips file actually contains before you share
one.
MIT. See LICENSE.