# Crashdx

> A deterministic crash diagnosis engine for .ips reports (CLI + MCP server)

- **Type:** MCP server
- **Install:** `agentstack add mcp-r00tify-crashdx`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [r00tify](https://agentstack.voostack.com/s/r00tify)
- **Installs:** 0
- **Category:** [Developer Tools](https://agentstack.voostack.com/c/developer-tools)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [r00tify](https://github.com/r00tify)
- **Source:** https://github.com/r00tify/crashdx

## Install

```sh
agentstack add mcp-r00tify-crashdx
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# crashdx

[](https://github.com/r00tify/crashdx/actions/workflows/ci.yml)

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.

## Why

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:

- **Every claim is traceable.** Facts carry a JSON path into the original report, so any
  verdict can be checked rather than trusted.
- **It says "inconclusive" when it is.** A verdict requires the leading hypothesis to be
  strongly supported *and* clearly ahead of the runner-up; otherwise you get the ranked
  candidates and what would settle it.

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](docs/DESIGN.md) for the full architecture.

## Example output

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.

### A verdict

```
$ crashdx analyze Tests/CrashDXCoreTests/Fixtures/nsexcrash.ips \
      --dsym Tests/CrashDXCoreTests/Fixtures/nsexcrash.dSYM

process:    nsexcrash
bug_type:   309
os:         macOS 26.3.1 (25D2128)
exception:  EXC_CRASH (SIGABRT)
terminated: Abort trap: 6
faulting thread (15 frames):
  libsystem_kernel.dylib  __pthread_kill
  libsystem_pthread.dylib  pthread_kill
  libsystem_c.dylib  abort
  libc++abi.dylib  __abort_message
  libc++abi.dylib  demangling_terminate_handler()
  libobjc.A.dylib  _objc_terminate()
  libc++abi.dylib  std::__terminate(void (*)())
  libc++abi.dylib  __cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*)
  libc++abi.dylib  __cxa_throw
  libobjc.A.dylib  objc_exception_throw
  CoreFoundation  -[NSException raise]
  nsexcrash  throwingHelper() (main.swift:8)
  nsexcrash  doWork() (main.swift:12)
  nsexcrash  main (main.swift:15)
  dyld  start
last exception backtrace (7 frames):
  CoreFoundation  __exceptionPreprocess
  libobjc.A.dylib  objc_exception_throw
  CoreFoundation  -[NSException raise]
  nsexcrash  throwingHelper() (main.swift:8)
  nsexcrash  doWork() (main.swift:12)
  nsexcrash  main (main.swift:15)
  dyld  start
symbolication (engine: crashSymbolicator):
  nsexcrash  symbolicated
  libsystem_kernel.dylib  no_dsym
  libsystem_pthread.dylib  no_dsym
  ...
DIAGNOSIS: Uncaught Objective-C exception (NSException)   (strong, score 5)
  A lastExceptionBacktrace is present and an objc_exception_throw frame appears in it (or on the
  faulting thread) — together these are pathognomonic for an uncaught NSException: `-raise` walked up
  through objc_exception_throw and was never caught, so the runtime called terminate and aborted. The
  asi "Terminating app due to uncaught exception" message is only corroboration when present; plain
  CLI/Foundation processes never write it, so its absence is not evidence against this hypothesis.
  evidence: lastExceptionBacktrace is present with 7 frame(s); lastExceptionBacktrace frame 1 matches
            sentinel 'objc-exception-throw': objc_exception_throw; Faulting thread frame 9 matches
            sentinel 'objc-exception-throw': objc_exception_throw
  inspect:  nsexcrash throwingHelper() (main.swift:8)
  confirm:  Symbolicate the app frames in lastExceptionBacktrace to find the throw site; Check the
            asi/console log for the exception name and reason, if available
  also considered: cxx-terminate (moderate, 2), abort-generic (weak, 1)
```

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.

### An inconclusive result

When the leading hypothesis is not strongly supported *and* clearly ahead, you get the
candidates instead of a label:

```
$ crashdx analyze Tests/CrashDXCoreTests/Fixtures/synthetic/wild-address.ips \
      --dsym corpus/fixtures/nullderef/nullderef.dSYM

process:    synthetic-wild-address
bug_type:   309
os:         macOS 26.3.1 (25D2128)
exception:  EXC_BAD_ACCESS (SIGSEGV)
terminated: Segmentation fault: 11
faulting thread (4 frames):
  nullderef  readThroughNullPointer() (main.swift:9)
  nullderef  run() (main.swift:13)
  nullderef  main (main.swift:16)
  dyld  start
symbolication (engine: crashSymbolicator):
  nullderef  symbolicated
  dyld  no_dsym
DIAGNOSIS: INCONCLUSIVE — competing hypotheses:
  1. Wild pointer or use-after-free [wild-or-uaf-address]   (moderate, score 3)
     The faulting address is outside the null page, and vmregioninfo reports it is not inside any known
     VM region at all — consistent with either a WILD pointer (an uninitialized or garbage value used as
     an address) or a USE-AFTER-FREE into memory the OS has since unmapped. This is deliberately a
     lower-confidence hypothesis than null-dereference: absence of region information doesn't distinguish
     between these two causes.
     evidence: Faulting address: 0x1deadbeef; Exception type: EXC_BAD_ACCESS
     inspect:  nullderef readThroughNullPointer() (main.swift:9)
     confirm:  Re-run with Address Sanitizer enabled to catch the use-after-free at the free/access site;
               Enable NSZombies (Malloc Scribble/Guard Malloc) to turn this into an immediate,
               informative crash; Profile with Instruments' Allocations tool, recording reference counts
```

### Other crash families

The same shape applies across the rule set. Verdict and evidence lines from four more
fixtures, with the explanation and follow-up steps elided:

```
# synthetic/watchdog-scene-create.ips
DIAGNOSIS: Watchdog timeout (main thread stall)   (strong, score 6)
  evidence: Termination code: 2343432205 (0x8badf00d); Termination namespace: FRONTBOARD; Watchdog
            event: scene-create, allowance 19.97s

# synthetic/jetsam-per-process-limit.ips
DIAGNOSIS: Jetsam memory kill   (strong, score 5)
  evidence: Exception subtype: RESOURCE_TYPE_MEMORY; Jetsam per-process-limit indicator:
            per-process-limit 350808KB exceeds task limit 335872KB

# synthetic/stack-overflow.ips
DIAGNOSIS: Stack overflow   (strong, score 5)
  evidence: Faulting address 0x3000 is near the stack region: vmregioninfo names a STACK GUARD region;
            within 500 bytes of sp (0x31f4); Faulting thread has 5 consecutive frames with identical
            symbol 'recurse(_:)' starting at frame 0 — a recursion signature

# crashspike-unsymbolicated.ips, with --dsym Tests/CrashDXCoreTests/Fixtures/crashspike.dSYM
DIAGNOSIS: Swift runtime fatal trap   (strong, score 6)
  evidence: Exception type: EXC_BREAKPOINT; Signal: SIGTRAP; Faulting thread frame 0 matches sentinel
            'assertion-failure': _assertionFailure(_:_:file:line:flags:)
```

Each of those also prints the full explanation, `inspect` point, and `confirm` steps shown
in the first example.

### JSON

`--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:

```jsonc
{
  "schemaVersion": "0.2",
  "tier": "summary",
  "diagnosis": {
    "status": "verdict",
    "verdict": {
      "id": "uncaught-objc-exception",
      "title": "Uncaught Objective-C exception (NSException)",
      "category": "objc-exception",
      "explanation": "A lastExceptionBacktrace is present and an objc_exception_throw frame ...",
      "supporting": [
        { "factID": "leb.present", "weight": 1 },
        { "factID": "leb.sentinel.objc-exception-throw", "weight": 3 },
        { "factID": "frames.sentinel.objc-exception-throw", "weight": 1 }
      ],
      "contradicting": [],
      "inspect": [
        { "frameIndex": 3, "leb": true, "symbol": "throwingHelper()",
          "sourceFile": "main.swift", "sourceLine": 8 }
      ],
      "confirmFurtherBy": [ /* ... */ ]
    },
    "hypotheses": [ /* all 3, each with its band + score */ ]
  },
  "event": {
    "bugType": "309", "exceptionType": "EXC_CRASH", "signal": "SIGABRT",
    "terminationIndicator": "Abort trap: 6", "terminationNamespace": "SIGNAL"
  },
  "faultingThread": { "index": 0, "queue": "com.apple.main-thread", "triggered": true,
                      "frames": [ /* ... */ ] },
  "symbolication": { "engine": "crashSymbolicator", "images": [ /* per-image outcome + uuid */ ] },
  "process": { "name": "nsexcrash", "osVersion": "macOS 26.3.1 (25D2128)",
               "captureTime": "2026-07-23 00:22:01.2793 +0000" }
}
```

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](#report-tiers)) adds `diagnosis.factsConsidered`,
which resolves every `factID` to its human-readable statement and its `sourcePath` into the
raw report:

```jsonc
{ "id": "leb.sentinel.objc-exception-throw",
  "statement": "lastExceptionBacktrace frame 1 matches sentinel 'objc-exception-throw': objc_exception_throw",
  "sourcePath": "lastExceptionBacktrace[1]" }
```

`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.

## Requirements

- macOS 14+
- **Swift 6.2+** toolchain. The manifest itself is `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`.
- **Xcode** (not just Command Line Tools): symbolication drives Apple's
  `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.

### Supported reports

Honest scope, because a crash diagnoser that quietly does worse on your platform is worse
than one that says so:

- **arm64 crash reports are fully supported**: Apple silicon Macs, iOS/iPadOS/watchOS/
  tvOS devices, and Simulators hosted on Apple silicon. This is what the fixture corpus
  covers.
- **x86_64 reports** (Intel Macs, Rosetta-translated processes, Simulators on Intel) parse
  and symbolicate, and the engine reads `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.
- **Only crash reports** (`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.

### Privacy

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.

## Install / build

```sh
git clone https://github.com/r00tify/crashdx.git
cd crashdx
swift build -c release
```

The binaries land in `.build/release/`. To put them on your `PATH`:

```sh
sudo mkdir -p /usr/local/bin
sudo cp .build/release/crashdx .build/release/crashdx-mcp /usr/local/bin/
```

(Or copy them somewhere you already own, such as `~/.local/bin`.)

## Usage

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.

```sh
crashdx analyze report.ips                           # human-readable summary + diagnosis
crashdx analyze report.ips --json                    # structured AnalysisReport JSON
crashdx analyze report.ips --json --tier full        # ...with every thread included
crashdx symbolicate report.ips --dsym path/to.dSYM   # just symbolicate, print enriched .ips
crashdx --version
```

`--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  --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.

### Report tiers

`--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 |

### Exit codes

`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`).

### MCP server

`crashdx-mcp` exposes the same pipeline over stdio (newline-delimited JSON-RPC) as the
`crashdx_analyze` and `crashdx_symbolicate` tools. With Claude Code:

```sh
claude mcp add crashdx /usr/local/bin/crashdx-mcp
```

A

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [r00tify](https://github.com/r00tify)
- **Source:** [r00tify/crashdx](https://github.com/r00tify/crashdx)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-r00tify-crashdx
- Seller: https://agentstack.voostack.com/s/r00tify
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
