Xcode Doctor: Static Analysis Engine for Xcode Configuration
Catching signing errors, entitlement gaps, and Watch/Widget target mismatches before App Store submission — by parsing .xcodeproj XML directly, in under 2 seconds, with zero writes and zero telemetry.
Stack
SwiftUI · macOS Native
Platform
macOS 13.0+
Performance
9 checks in < 2 seconds
Distribution
Apple-notarized binary
Context
iOS and macOS developers encounter a recurring class of failure: the build works locally, tests pass, but App Store submission fails due to a configuration issue that Xcode never flagged. Signing certificate mismatches, missing entitlement files, Watch companion app linking failures, Widget extension bundle ID conflicts — these errors surface only after a complete build cycle and submission review.
The review turnaround from Apple is typically 24–48 hours per correction cycle. A single configuration issue discovered post-submission can delay a release by days.
Problem
The root cause is architectural: Xcode surfaces configuration errors reactively — at build time or submission time. The project configuration itself — the project.pbxproj file, entitlement plists, signing settings — is inspectable at any time. But manually auditing these files is slow, requires deep knowledge of the format, and is error-prone enough that most developers don't do it.
There was no tool that statically analyzed Xcode project configuration before a build — the equivalent of a linter for project structure rather than source code.
Constraints
- —Read-only by design — the tool must never write to or modify project files, even unintentionally. Any diagnostic tool that can corrupt a project is worse than no tool.
- —Analysis must complete in under 2 seconds — slow enough to feel like a build step defeats the purpose of running before a build
- —Zero telemetry — .xcodeproj files contain proprietary app structure, team IDs, bundle identifiers, and signing configurations
- —Apple notarization required for Gatekeeper-compliant distribution without forcing users to override security settings
- —Must work without Xcode open and without invoking any Xcode CLI tools — the analysis engine must be self-contained
Architecture: The Static Analysis Engine
Parsing the .xcodeproj Format
An .xcodeproj bundle contains a project.pbxproj file in Apple's custom property list format. This is not standard XML — it uses a hybrid format that resembles a serialized object graph. The analysis engine parses this directly using PropertyListSerialization, then traverses the object graph to extract the full target dependency tree, build settings per configuration, entitlement file paths, bundle identifiers, and capability flags.
The 9 Checks
Each check is an independent diagnostic rule applied to the parsed project graph:
- 1.Signing certificate presence and validity across Debug/Release configurations
- 2.Entitlement file existence and path resolution for all targets
- 3.Watch companion app bundle ID consistency with the host app
- 4.Widget extension bundle ID prefixing rules (must nest under parent bundle ID)
- 5.App Group entitlement coverage for extensions sharing data with the host app
- 6.Deployment target alignment across targets in the same scheme
- 7.Capability entitlement gaps (e.g., iCloud capability enabled but container not configured)
- 8.Framework embedding settings for dynamic frameworks that must be stripped for extensions
- 9.Code signing identity consistency between targets in a multi-target scheme
Severity Model
Each finding is classified into three severity levels. Errors are issues that will cause a build failure or App Store rejection — fix before submitting. Warnings are non-blocking but will cause problems under specific conditions (e.g., a missing entitlement that only fails on device, not simulator). Info findings are observations: configuration patterns that are valid but unusual. Each finding includes an actionable description — not just what's wrong but what to change in Xcode to fix it.
Implementation: Bundle ID Consistency Check
The WatchKit companion bundle ID check — one of the most common App Store rejection causes — validates that the Watch app's bundle ID is exactly [parent].watchkitapp and the Watch extension's ID is [parent].watchkitapp.watchkitextension:
struct WatchBundleIDCheck: DiagnosticCheck {
func run(on project: ParsedProject) -> [DiagnosticFinding] {
var findings: [DiagnosticFinding] = []
guard let watchTarget = project.targets
.first(where: { $0.productType == .watchKitApp }),
let hostTarget = project.targets
.first(where: { $0.dependencies.contains(watchTarget.id) })
else { return findings }
let hostBundleID = hostTarget.buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] ?? ""
let watchBundleID = watchTarget.buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] ?? ""
let expectedWatchID = "\(hostBundleID).watchkitapp"
if watchBundleID != expectedWatchID {
findings.append(.init(
severity: .error,
targetName: watchTarget.name,
message: "Watch app bundle ID '\(watchBundleID)' must be '\(expectedWatchID)'",
resolution: "In the Watch app target's Build Settings, set " +
"PRODUCT_BUNDLE_IDENTIFIER to \(expectedWatchID)"
))
}
return findings
}
}Solution Highlights
- •Static .xcodeproj XML/plist analysis — no Xcode dependency, no build invocation needed
- •9 checks completing in under 2 seconds on any modern Mac
- •Read-only sandboxed access — write operations architecturally prevented at the file system level
- •Severity-classified findings with per-issue actionable resolution instructions
- •Zero telemetry — all analysis is local, no network requests of any kind
- •Apple notarization with SHA-256 checksum for verifiable download integrity
Outcome
Distributed as an Apple-notarized macOS binary, downloadable without Gatekeeper friction. iOS developers use it as a pre-submission checklist step — run Xcode Doctor, fix reported issues, then submit.
- →9 configuration checks in under 2 seconds — faster than a clean build by two orders of magnitude
- →Catches signing and entitlement errors before App Store submission, avoiding 24–48 hour rejection cycles
- →Configuration debugging time reduced from hours of manual .xcodeproj inspection to a single tool run
- →Zero false positives on correctly configured projects — informational findings only
- →Fully offline, zero telemetry — safe to run on client projects with NDA-covered bundle IDs
"The constraint that drove the architecture: parsing had to be deterministic and zero-side-effect. The tool reads project structure — it never writes, never invokes build commands, never accesses the network."
Key Technical Learnings
The pbxproj format is not stable
Apple changes the .xcodeproj internal format across Xcode versions. The analysis engine pins to known field names with fallback handling for renamed keys — any check that hard-assumes a specific pbxproj version will silently fail when Xcode updates.
Build configurations double the check surface
Each target has separate build settings for Debug and Release configurations. A signing identity set correctly in Debug may be missing in Release. Every check that validates build settings must iterate across all configurations, not just the active one.
Actionable output matters more than completeness
An error message that says 'bundle ID mismatch' is useless. An error that says 'Watch app bundle ID is X, must be Y — change PRODUCT_BUNDLE_IDENTIFIER in the Watch app target Build Settings' is actionable. The UX of diagnostic output is as important as the accuracy of the checks.
Notarization requires app sandbox compliance
Notarized apps distributed outside the Mac App Store must have the Hardened Runtime entitlement enabled and cannot use APIs blocked by the app sandbox. This affects file access patterns — the tool uses NSOpenPanel for project selection rather than direct path access, which is the correct model for a sandboxed tool anyway.
Technical FAQ
Can Xcode Doctor be run in a CI pipeline?↓
Why doesn't Xcode already do this?↓
What makes a static analysis approach better than build-time errors?↓
Getting App Store rejections from configuration errors?
An Architecture Audit covers signing configuration, entitlement setup, and target dependency structure — catching these issues before submission.