{
  "markdown": "# crashdx\n\n[![CI](https://github.com/r00tify/crashdx/actions/workflows/ci.yml/badge.svg)](https://github.com/r00tify/crashdx/actions/workflows/ci.yml)\n\nA crash **diagnosis** engine for Apple platforms: parses `.ips` crash reports, symbolicates\nthem against dSYMs, and produces an evidence-cited, ranked diagnosis, not just a\nsymbolicated stack trace.\n\nShips as a dependency-free Swift library (`CrashDXCore` imports only Foundation), a CLI\n(`crashdx`), and an MCP server (`crashdx-mcp`), so agents and humans use the same engine.\n\n## Why\n\nA symbolicated stack trace tells you where a process died, not why. crashdx adds a second\nstage on top of symbolication: deterministic, rule-based evidence extraction (watchdog\nbudgets, jetsam tables, register and memory state, `lastExceptionBacktrace`/`asi`) feeding\na ranked set of competing hypotheses, each citing the specific facts that support it and\npointing back into the raw report.\n\nTwo properties it holds onto deliberately:\n\n- **Every claim is traceable.** Facts carry a JSON path into the original report, so any\n  verdict can be checked rather than trusted.\n- **It says \"inconclusive\" when it is.** A verdict requires the leading hypothesis to be\n  strongly supported *and* clearly ahead of the runner-up; otherwise you get the ranked\n  candidates and what would settle it.\n\nThere are no LLM calls in the engine: it produces verifiable facts and ranked\ninterpretations, and leaves the narrative to whatever consumes them. See\n[docs/DESIGN.md](docs/DESIGN.md) for the full architecture.\n\n## Example output\n\nEverything below is real, unedited output from fixtures in this repo (elisions are marked).\nPaths are relative to the repo root, and the `--dsym` flags point at dSYMs the repo ships,\nso these reproduce as-is; use `swift run crashdx ...` if you have not installed the binary.\n\n### A verdict\n\n```\n$ crashdx analyze Tests/CrashDXCoreTests/Fixtures/nsexcrash.ips \\\n      --dsym Tests/CrashDXCoreTests/Fixtures/nsexcrash.dSYM\n\nprocess:    nsexcrash\nbug_type:   309\nos:         macOS 26.3.1 (25D2128)\nexception:  EXC_CRASH (SIGABRT)\nterminated: Abort trap: 6\nfaulting thread (15 frames):\n  libsystem_kernel.dylib  __pthread_kill\n  libsystem_pthread.dylib  pthread_kill\n  libsystem_c.dylib  abort\n  libc++abi.dylib  __abort_message\n  libc++abi.dylib  demangling_terminate_handler()\n  libobjc.A.dylib  _objc_terminate()\n  libc++abi.dylib  std::__terminate(void (*)())\n  libc++abi.dylib  __cxxabiv1::failed_throw(__cxxabiv1::__cxa_exception*)\n  libc++abi.dylib  __cxa_throw\n  libobjc.A.dylib  objc_exception_throw\n  CoreFoundation  -[NSException raise]\n  nsexcrash  throwingHelper() (main.swift:8)\n  nsexcrash  doWork() (main.swift:12)\n  nsexcrash  main (main.swift:15)\n  dyld  start\nlast exception backtrace (7 frames):\n  CoreFoundation  __exceptionPreprocess\n  libobjc.A.dylib  objc_exception_throw\n  CoreFoundation  -[NSException raise]\n  nsexcrash  throwingHelper() (main.swift:8)\n  nsexcrash  doWork() (main.swift:12)\n  nsexcrash  main (main.swift:15)\n  dyld  start\nsymbolication (engine: crashSymbolicator):\n  nsexcrash  symbolicated\n  libsystem_kernel.dylib  no_dsym\n  libsystem_pthread.dylib  no_dsym\n  ...\nDIAGNOSIS: Uncaught Objective-C exception (NSException)   (strong, score 5)\n  A lastExceptionBacktrace is present and an objc_exception_throw frame appears in it (or on the\n  faulting thread) — together these are pathognomonic for an uncaught NSException: `-raise` walked up\n  through objc_exception_throw and was never caught, so the runtime called terminate and aborted. The\n  asi \"Terminating app due to uncaught exception\" message is only corroboration when present; plain\n  CLI/Foundation processes never write it, so its absence is not evidence against this hypothesis.\n  evidence: lastExceptionBacktrace is present with 7 frame(s); lastExceptionBacktrace frame 1 matches\n            sentinel 'objc-exception-throw': objc_exception_throw; Faulting thread frame 9 matches\n            sentinel 'objc-exception-throw': objc_exception_throw\n  inspect:  nsexcrash throwingHelper() (main.swift:8)\n  confirm:  Symbolicate the app frames in lastExceptionBacktrace to find the throw site; Check the\n            asi/console log for the exception name and reason, if available\n  also considered: cxx-terminate (moderate, 2), abort-generic (weak, 1)\n```\n\nThe `evidence` line cites the supporting facts the rule scored on, up to four, with a\nrunning total when there are more; each fact carries a path back into the original report,\nwhich `--json --tier standard` exposes. `also considered` is the full remainder of the\nranked list, so you can see what the verdict beat and by how much.\n\n### An inconclusive result\n\nWhen the leading hypothesis is not strongly supported *and* clearly ahead, you get the\ncandidates instead of a label:\n\n```\n$ crashdx analyze Tests/CrashDXCoreTests/Fixtures/synthetic/wild-address.ips \\\n      --dsym corpus/fixtures/nullderef/nullderef.dSYM\n\nprocess:    synthetic-wild-address\nbug_type:   309\nos:         macOS 26.3.1 (25D2128)\nexception:  EXC_BAD_ACCESS (SIGSEGV)\nterminated: Segmentation fault: 11\nfaulting thread (4 frames):\n  nullderef  readThroughNullPointer() (main.swift:9)\n  nullderef  run() (main.swift:13)\n  nullderef  main (main.swift:16)\n  dyld  start\nsymbolication (engine: crashSymbolicator):\n  nullderef  symbolicated\n  dyld  no_dsym\nDIAGNOSIS: INCONCLUSIVE — competing hypotheses:\n  1. Wild pointer or use-after-free [wild-or-uaf-address]   (moderate, score 3)\n     The faulting address is outside the null page, and vmregioninfo reports it is not inside any known\n     VM region at all — consistent with either a WILD pointer (an uninitialized or garbage value used as\n     an address) or a USE-AFTER-FREE into memory the OS has since unmapped. This is deliberately a\n     lower-confidence hypothesis than null-dereference: absence of region information doesn't distinguish\n     between these two causes.\n     evidence: Faulting address: 0x1deadbeef; Exception type: EXC_BAD_ACCESS\n     inspect:  nullderef readThroughNullPointer() (main.swift:9)\n     confirm:  Re-run with Address Sanitizer enabled to catch the use-after-free at the free/access site;\n               Enable NSZombies (Malloc Scribble/Guard Malloc) to turn this into an immediate,\n               informative crash; Profile with Instruments' Allocations tool, recording reference counts\n```\n\n### Other crash families\n\nThe same shape applies across the rule set. Verdict and evidence lines from four more\nfixtures, with the explanation and follow-up steps elided:\n\n```\n# synthetic/watchdog-scene-create.ips\nDIAGNOSIS: Watchdog timeout (main thread stall)   (strong, score 6)\n  evidence: Termination code: 2343432205 (0x8badf00d); Termination namespace: FRONTBOARD; Watchdog\n            event: scene-create, allowance 19.97s\n\n# synthetic/jetsam-per-process-limit.ips\nDIAGNOSIS: Jetsam memory kill   (strong, score 5)\n  evidence: Exception subtype: RESOURCE_TYPE_MEMORY; Jetsam per-process-limit indicator:\n            per-process-limit 350808KB exceeds task limit 335872KB\n\n# synthetic/stack-overflow.ips\nDIAGNOSIS: Stack overflow   (strong, score 5)\n  evidence: Faulting address 0x3000 is near the stack region: vmregioninfo names a STACK GUARD region;\n            within 500 bytes of sp (0x31f4); Faulting thread has 5 consecutive frames with identical\n            symbol 'recurse(_:)' starting at frame 0 — a recursion signature\n\n# crashspike-unsymbolicated.ips, with --dsym Tests/CrashDXCoreTests/Fixtures/crashspike.dSYM\nDIAGNOSIS: Swift runtime fatal trap   (strong, score 6)\n  evidence: Exception type: EXC_BREAKPOINT; Signal: SIGTRAP; Faulting thread frame 0 matches sentinel\n            'assertion-failure': _assertionFailure(_:_:file:line:flags:)\n```\n\nEach of those also prints the full explanation, `inspect` point, and `confirm` steps shown\nin the first example.\n\n### JSON\n\n`--json` emits the same analysis as a structured `AnalysisReport`, which is what the MCP\nserver returns and what you would parse in CI. Below is the same crash as the first\nexample, abridged (`/* ... */` marks elisions). The real output is a single minified line\nwith sorted keys, so pipe it through `jq` or `python3 -m json.tool`; it is pretty-printed\nand reordered here for readability:\n\n```jsonc\n{\n  \"schemaVersion\": \"0.2\",\n  \"tier\": \"summary\",\n  \"diagnosis\": {\n    \"status\": \"verdict\",\n    \"verdict\": {\n      \"id\": \"uncaught-objc-exception\",\n      \"title\": \"Uncaught Objective-C exception (NSException)\",\n      \"category\": \"objc-exception\",\n      \"explanation\": \"A lastExceptionBacktrace is present and an objc_exception_throw frame ...\",\n      \"supporting\": [\n        { \"factID\": \"leb.present\", \"weight\": 1 },\n        { \"factID\": \"leb.sentinel.objc-exception-throw\", \"weight\": 3 },\n        { \"factID\": \"frames.sentinel.objc-exception-throw\", \"weight\": 1 }\n      ],\n      \"contradicting\": [],\n      \"inspect\": [\n        { \"frameIndex\": 3, \"leb\": true, \"symbol\": \"throwingHelper()\",\n          \"sourceFile\": \"main.swift\", \"sourceLine\": 8 }\n      ],\n      \"confirmFurtherBy\": [ /* ... */ ]\n    },\n    \"hypotheses\": [ /* all 3, each with its band + score */ ]\n  },\n  \"event\": {\n    \"bugType\": \"309\", \"exceptionType\": \"EXC_CRASH\", \"signal\": \"SIGABRT\",\n    \"terminationIndicator\": \"Abort trap: 6\", \"terminationNamespace\": \"SIGNAL\"\n  },\n  \"faultingThread\": { \"index\": 0, \"queue\": \"com.apple.main-thread\", \"triggered\": true,\n                      \"frames\": [ /* ... */ ] },\n  \"symbolication\": { \"engine\": \"crashSymbolicator\", \"images\": [ /* per-image outcome + uuid */ ] },\n  \"process\": { \"name\": \"nsexcrash\", \"osVersion\": \"macOS 26.3.1 (25D2128)\",\n               \"captureTime\": \"2026-07-23 00:22:01.2793 +0000\" }\n}\n```\n\nNote `\"leb\": true` on the inspect point: the throw site was recovered from\n`lastExceptionBacktrace` rather than the faulting thread, and the report says so instead of\nflattening the distinction.\n\n`--tier standard` (see [Report tiers](#report-tiers)) adds `diagnosis.factsConsidered`,\nwhich resolves every `factID` to its human-readable statement and its `sourcePath` into the\nraw report:\n\n```jsonc\n{ \"id\": \"leb.sentinel.objc-exception-throw\",\n  \"statement\": \"lastExceptionBacktrace frame 1 matches sentinel 'objc-exception-throw': objc_exception_throw\",\n  \"sourcePath\": \"lastExceptionBacktrace[1]\" }\n```\n\n`sourcePath` is not optional on a `Fact`, so every fact the diagnosis cites can be walked\nback to the part of the report it was read from.\n\nCommitted golden snapshots of both\ntiers live in `Tests/CrashDXCoreTests/Fixtures/` (`nsexcrash-summary-golden.json`,\n`nullderef-standard-golden.json`) if you want the complete, unabridged shape.\n\n## Requirements\n\n- macOS 14+\n- **Swift 6.2+** toolchain. The manifest itself is `swift-tools-version:6.0`, but the MCP\n  server's pinned dependencies declare up to `6.2`, and SwiftPM resolves the whole\n  package graph, so 6.2 is the real floor for every target, including `CrashDXCore`.\n- **Xcode** (not just Command Line Tools): symbolication drives Apple's\n  `CrashSymbolicator.py` from Xcode's `CoreSymbolicationDT.framework`, located via\n  `xcode-select -p`. Without it, crashdx falls back to `atos`, which resolves fewer\n  source locations. Parsing and diagnosis work either way.\n- `/usr/bin/python3` and `/usr/bin/dwarfdump` (both provided by Xcode's command line\n  tools), used to run `CrashSymbolicator.py` and to verify dSYM UUIDs.\n\n### Supported reports\n\nHonest scope, because a crash diagnoser that quietly does worse on your platform is worse\nthan one that says so:\n\n- **arm64 crash reports are fully supported**: Apple silicon Macs, iOS/iPadOS/watchOS/\n  tvOS devices, and Simulators hosted on Apple silicon. This is what the fixture corpus\n  covers.\n- **x86_64 reports** (Intel Macs, Rosetta-translated processes, Simulators on Intel) parse\n  and symbolicate, and the engine reads `x86_THREAD_STATE` registers and the 4 KB page\n  size. There is no Intel fixture in the corpus, so this path is reasoned-about rather\n  than exercised against a real report.\n- **Only crash reports** (`bug_type` 309/109). Jetsam event reports (`JetsamEvent-*.ips`,\n  `bug_type` 298) and hang/stackshot reports (`bug_type` 288) use a different payload\n  shape with no threads or exception object; crashdx parses them without error but has\n  no facts to work from and will report inconclusive.\n\n### Privacy\n\ncrashdx runs entirely on your machine and **makes no network calls of any kind**. Crash\nreports, dSYMs, and everything derived from them stay local. This matters because `.ips`\nfiles contain identifying data (`crashReporterKey`, boot/sleep-wake UUIDs, device model,\nand usernames in paths), so if you attach one to a bug report, scrub it first.\n\n## Install / build\n\n```sh\ngit clone https://github.com/r00tify/crashdx.git\ncd crashdx\nswift build -c release\n```\n\nThe binaries land in `.build/release/`. To put them on your `PATH`:\n\n```sh\nsudo mkdir -p /usr/local/bin\nsudo cp .build/release/crashdx .build/release/crashdx-mcp /usr/local/bin/\n```\n\n(Or copy them somewhere you already own, such as `~/.local/bin`.)\n\n## Usage\n\nCrash reports live in `~/Library/Logs/DiagnosticReports/` on macOS (or Console.app →\nCrash Reports); on iOS, Settings → Privacy & Security → Analytics & Improvements →\nAnalytics Data, and Xcode's Organizer for TestFlight/App Store crashes.\n\n```sh\ncrashdx analyze report.ips                           # human-readable summary + diagnosis\ncrashdx analyze report.ips --json                    # structured AnalysisReport JSON\ncrashdx analyze report.ips --json --tier full        # ...with every thread included\ncrashdx symbolicate report.ips --dsym path/to.dSYM   # just symbolicate, print enriched .ips\ncrashdx --version\n```\n\n`--dsym` accepts a `.dSYM` bundle, an `.xcarchive`, or a directory to search recursively\n(repeatable), and `--no-spotlight` skips Spotlight-based discovery while `--no-archives`\nskips Xcode's archive directory. Run `crashdx <subcommand> --help` for the full option\nlist. Note that `--json` and `--tier` apply to `analyze` only; `symbolicate` always emits\na complete `.ips`.\n\nBoth subcommands search Spotlight and Xcode's archives for a matching dSYM automatically.\nFor foreign reports (user-submitted, TestFlight, CI artifacts) pass the build's dSYM\nexplicitly with `--dsym`.\n\nStrings taken from the report (process name, symbols, paths, exception text) are escaped\nbefore the human-readable summary prints them: C0 controls, DEL, and bidi overrides\nrender as `\\x0A` / `\\u{202E}` instead of being emitted, so a crafted report cannot forge\na `DIAGNOSIS:` line or repaint your terminal. `--json` and `symbolicate` emit the\nreport's strings verbatim; treat their output as data, not as terminal text.\n\nA dSYM from a *different* build is refused rather than used: a stale dSYM produces\nconfidently wrong symbols, which is worse than none. That case is reported as\n`uuid_mismatch` (with the offending path in `reason`), kept distinct from `no_dsym`\nbecause the fix is different: find the archive matching the report's build UUID, rather\nthan go looking for a file you may already have.\n\n### Report tiers\n\n`--tier` controls how much of the report is emitted; it never changes the diagnosis,\nwhich is always computed from the full report:\n\n| Tier | Contents |\n|---|---|\n| `summary` (default) | Faulting thread (capped at 15 frames), `lastExceptionBacktrace`, and the diagnosis verdict + ranked hypotheses |\n| `standard` | Lifts the frame cap, and adds the binary images, other threads that contain app frames, and `factsConsidered` (the evidence behind each hypothesis) |\n| `full` | Adds every remaining thread, including those with no app frames |\n\n### Exit codes\n\n`0` success · `64` usage error · `65` the input could not be parsed as an `.ips` report,\nor symbolication failed · `66` file not found or unreadable.\n\nDiagnostics go to stderr; stdout carries report output (and `--help`/`--version`).\n\n### MCP server\n\n`crashdx-mcp` exposes the same pipeline over stdio (newline-delimited JSON-RPC) as the\n`crashdx_analyze` and `crashdx_symbolicate` tools. With Claude Code:\n\n```sh\nclaude mcp add crashdx /usr/local/bin/crashdx-mcp\n```\n\nA matching `crash-triage` Agent Skill ships in `.claude/skills/crash-triage/`, covering\nhow to read the diagnosis and the interpretation pitfalls specific to each crash family.\n\n### As a library\n\nAdd `https://github.com/r00tify/crashdx.git` to your `Package.swift`, depend on the\n`CrashDXCore` product, and call\n`AnalyzePipeline.analyze(path:tier:dsymPaths:useSpotlight:searchArchives:)`, the same\nentry point both binaries use.\n\n`CrashDXCore` imports nothing but `Foundation`, but two limitations are worth knowing\nbefore you depend on it:\n\n- **It is macOS-only.** Symbolication shells out to `atos`/`dwarfdump`/`mdfind` through\n  `Foundation.Process`, which does not exist on iOS. Adding this package to an iOS target\n  fails during dependency resolution, before anything compiles.\n- SwiftPM resolves the whole package graph rather than a single product, so depending on\n  this package pins the MCP server's dependencies (swift-sdk and its transitive NIO/\n  collections/log packages) in your `Package.resolved` even if you never import them.\n\n`AnalysisReport` and the diagnosis model are `Sendable`, so analysing a directory of\nreports concurrently works as you would expect.\n\n## Project layout\n\n```\nSources/CrashDXCore/   Parsing, symbolication, and the diagnosis engine (imports only Foundation)\nSources/crashdx/       CLI\nSources/crashdx-mcp/   MCP server (depends on swift-sdk)\nTests/                 Unit + integration tests, golden snapshots, crash fixtures\ncorpus/                Ground-truth crash fixtures and their verified findings\ndocs/DESIGN.md         Diagnosis engine architecture\nScripts/               Fixture scrubbing and the CI privacy guard\n```\n\n## Testing\n\n```sh\nswift test\n```\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md). One rule matters more than the rest: **crash\nreports and dSYMs must be scrubbed before they are committed**. They carry device\nidentifiers and your username. `Scripts/scrub-fixture.py` handles `.ips` files and\n`Scripts/check-fixtures-scrubbed.sh` (also run in CI) will tell you if anything slipped\nthrough.\n\n## Security and privacy\n\ncrashdx makes no network calls; nothing leaves your machine. See [SECURITY.md](SECURITY.md)\nfor vulnerability reporting and for what a `.ips` file actually contains before you share\none.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n",
  "bytes": 18409,
  "sha": "7c2efa926e3551d6ec36f7a5180f0a8f3041a59d2bbe0c0e6fb3289b57daee7f",
  "repo_slug": "r00tify/crashdx",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_r00tify_crashdx_11e74496/readme"
}