A few days after shipping an update to one of my wallpaper apps, an unfamiliar row appeared in the Firebase Crashlytics dashboard. The crashes were being recorded, but every stack frame read like 0x0000000102f4c8a8 — just an address. There was no way to tell which function had failed or on which line. At the top sat a yellow "Missing dSYM" warning. I had a hunch about the cause: shortly before, I had moved Firebase from CocoaPods to Swift Package Manager.
Staring at that wall of hex, I thought of my two grandfathers, both temple carpenters. A joint that skips its finishing work looks fine from the outside, but the warp always shows up later. Crash reports are the same. Skip the unglamorous finishing step of symbolication, and the one time a user is actually in trouble, the part you need most is unreadable. After running my own apps since 2014, this "it runs but it is not finished" state is the one I fear most. This time I rebuilt that symbolication step, with Claude Code helping, until it passed reliably.
What a dSYM Actually Does
A dSYM (debug symbols) file is a dictionary that maps the function names, file names, and line numbers stripped from a release build back to the memory addresses in the executable. When a crash happens, Crashlytics collects the addresses and later consults this dictionary to restore something readable, like applyFilter(_:) at WallpaperViewModel.swift:142. That restoration is symbolication.
The mapping between the dictionary and the binary is keyed by UUID. So if the dSYM that reaches Crashlytics was not generated from the exact same build you shipped, the UUIDs will not line up and you get "Missing dSYM." Put the other way around: the cause is always one of two things — the dSYM was never created, or it was created but never delivered. Settling that split first was the shortest path through the problem.
Narrowing It to Two Options with Claude Code
I launched Claude Code from the terminal and started by handing it the UUID shown in the warning. Crashlytics tells you exactly which dSYM UUID it is missing, so the first job is to check whether that UUID exists locally. macOS has a built-in way to look up a dSYM by UUID.
# Find the UUID that Crashlytics is asking for, locally
mdfind "com_apple_xcode_dsym_uuids == E1B2C3D4-5678-90AB-CDEF-1234567890AB"
# Inspect the dSYM UUID inside the archive directly
dwarfdump --uuid ~/Library/Developer/Xcode/Archives/2026-06-01/MyApp.xcarchive/dSYMs/*.dSYMThis told me the dSYM had in fact been generated correctly inside the archive. So this was not the "never created" case — it was the "never delivered" case. The moment the cause collapsed to one of two options, the search space shrank dramatically. When I told Claude Code "the archive has a matching-UUID dSYM, so it is generated but not uploaded, and I suspect the SPM move changed the run script path," it proceeded on exactly that premise — which is one of the most fragile spots in an SPM migration.
The Run Script Path Had Changed in the SPM Move
Under CocoaPods, Crashlytics called Pods/FirebaseCrashlytics/run from a Build Phase script. Once you move to SPM, the Pods directory is gone, so a script still pointing at that old path simply does nothing. The nasty part is that it fails silently — the build stays green.
The correct SPM path points at the firebase-ios-sdk checkout under the build directory. I added a fresh Run Script in Build Phases and declared its input files explicitly.
# Build Phases > Run Script (SPM version of Crashlytics)
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"In the Input Files section, pass the locations of the dSYM and the Info.plist. Leave this empty and the new build system may skip the script entirely.
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}
$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)
I also confirmed that Debug Information Format for the Release configuration was set to DWARF with dSYM File. If it is set to plain DWARF, the dSYM is never produced in the first place. My project was fine here, but another of my wallpaper apps had this setting dropped only on Release — the textbook "never created" case. Same symptom, opposite cause.
Sending the Ones You Already Missed
Fixing the run script does not retroactively send the dSYMs for builds already on the store. Those you have to send by hand, calling the SPM version of upload-symbols directly.
# Use the upload-symbols binary from the SPM checkout
UPLOAD="$(find ~/Library/Developer/Xcode/DerivedData -name upload-symbols -path '*firebase-ios-sdk*' | head -1)"
"$UPLOAD" -gsp ./MyApp/GoogleService-Info.plist -p ios \
~/Library/Developer/Xcode/Archives/2026-06-01/MyApp.xcarchive/dSYMsWhen I have Claude Code assemble this, I usually ask it to "turn this into a small loop that processes several apps' archives at once." I run several apps in parallel, mostly wallpaper and relaxation titles, so sending them one at a time guarantees I will miss some. Walking the archive list and sending only the unsent ones made this easy to fold into the monthly update routine.
# Process recent archives in order (swap the plist per app)
for ARCHIVE in ~/Library/Developer/Xcode/Archives/2026-*/*.xcarchive; do
echo "▶ $(basename "$ARCHIVE")"
"$UPLOAD" -gsp ./MyApp/GoogleService-Info.plist -p ios "$ARCHIVE/dSYMs" || echo " skip"
doneAfter sending, it takes a while before the Crashlytics dashboard reflects the change. Do not resend repeatedly just because symbolication is not instant — waiting tens of minutes to a few hours is the right move. I once got impatient here and sent duplicates, which doubled the logs and made everything harder to verify. I will leave that here as a note to myself.
Letting Claude Code Read the Crash, Too
Once symbolication works, the stack trace reads in function names and line numbers — and this is exactly where Claude Code earns its keep. I paste the symbolicated trace and ask it to "name the conditions under which this crash fires and point to the likely spot." With raw addresses, any AI is just guessing; with function names and line numbers, it can actually open the relevant file and narrow down a hypothesis.
This particular crash came from image-filter code touching the UI off the main thread. Because the symbolicated trace showed applyFilter(_:), Claude Code opened the corresponding view model and flagged that the @MainActor guarantee was being dropped there. Had it stayed as raw addresses, I would probably have spent hours looking in the wrong place.
What I Will Do Next Time I See This Warning
If "Missing dSYM" shows up again, here is my order of operations. First, match the warning's UUID against my machine with mdfind and dwarfdump to settle whether it was "never created" or "never delivered." For the former, suspect the Release Debug Information Format; for the latter, suspect the run script path and Input Files. Run the manual upload exactly once, then wait for it to register. Just having that order decided keeps me from falling into the same pit twice.
The quiet finishing steps are the ones that pay off later. Keeping your crash reports readable is what lets you act before a user quietly walks away. If this adds one line to the monthly checklist of anyone juggling several apps the way I do, I will be glad.