●MODEL — Claude Sonnet 5 is the default across all plans and Claude Code at $2/$10 per million tokens through August 31●FABLE — Access to Claude Fable 5 and Mythos 5 is being restored from July 1 after US export controls were lifted●SCIENCE — Claude Science, an AI workbench for researchers, is in beta for Pro, Max, Team, and Enterprise●GATEWAY — The Claude apps gateway for Amazon Bedrock and Google Cloud adds SSO, policy, role-based access, and per-user cost attribution●ENTERPRISE — Claude Enterprise adds richer admin analytics, model-level entitlements, and spend alerts●CODE — Claude Sonnet 5 is now the default model in Claude Code with a native 1M-token context window●MODEL — Claude Sonnet 5 is the default across all plans and Claude Code at $2/$10 per million tokens through August 31●FABLE — Access to Claude Fable 5 and Mythos 5 is being restored from July 1 after US export controls were lifted●SCIENCE — Claude Science, an AI workbench for researchers, is in beta for Pro, Max, Team, and Enterprise●GATEWAY — The Claude apps gateway for Amazon Bedrock and Google Cloud adds SSO, policy, role-based access, and per-user cost attribution●ENTERPRISE — Claude Enterprise adds richer admin analytics, model-level entitlements, and spend alerts●CODE — Claude Sonnet 5 is now the default model in Claude Code with a native 1M-token context window
Auditing Your Own Code for Required Reason API Declarations with Claude Code
Required reason codes in PrivacyInfo.xcprivacy apply to your own code, not just third-party SDKs. After a wallpaper app of mine was rejected, here is the Claude Code workflow I use to scan Swift sources and match APIs to reason codes before submission.
Last spring I shipped an update to one of my wallpaper apps, and a few hours after submitting to the App Store a notice landed: "ITMS-91053: Missing API declaration." I had kept my dependency manifests tidy, so what went wrong? It was my own code. A stray UserDefaults call I had used for saving a setting counts as one of Apple's Required Reason APIs.
Conversation about Required Reason APIs tends to gravitate toward SDK compliance. But the same declaration rules apply to the code you write yourself. This piece narrows in on scanning your own sources — using Claude Code to inventory the gaps and fill in reason codes in PrivacyInfo.xcprivacy. For the dependency side, I leave you to my separate write-up on catching missing manifest declarations in dependencies.
Why your own code slips through
Required Reason APIs sound imposing, yet they are a collection of quiet, everyday calls. UserDefaults for saving settings, modificationDate for reading a file's timestamp, volumeAvailableCapacityKey for checking free space, systemUptime for measuring elapsed time on device. None of them carry the drama the word "privacy" suggests. That is exactly why the need to declare them never registers, and they sink deep into the codebase.
In my case there were three gaps: a UserDefaults write for an onboarding-complete flag, a modificationDate read that judged how fresh a cached image was, and a volumeAvailableCapacityForImportantUsageKey check before a download. All of them were written years ago, before this rule even existed. I had no appetite to trace every source file by hand, so I wanted a mechanical way to surface them.
The four categories and their reason codes
The categories you are most likely to trip over in your own code are these four. Each has reason codes defined by Apple, and .xcprivacy records both the category and the reason code.
Category (NSPrivacyAccessedAPIType)
Common APIs
Reason codes indie apps often use
UserDefaults
UserDefaults / NSUserDefaults
CA92.1 (used only within the app / App Group)
FileTimestamp
modificationDate / creationDate / stat
C617.1 (display to user) / 3B52.1 (within your container)
DiskSpace
volumeAvailableCapacity… / systemFreeSize
85F4.1 (display to user) / E174.1 (write to avoid low-space errors)
SystemBootTime
systemUptime / mach_absolute_time
35F9.1 (measure time between on-device events)
The reason codes read alike, so you cannot mechanically settle on a single one. The script only goes as far as proposing candidates; which reason actually fits your usage is a choice the developer makes. Always confirm against Apple's Required Reason API documentation.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A Python scanner that finds Required Reason APIs across four categories — UserDefaults, file timestamp, disk space, boot time — in your own Swift code
✦How to map detected symbols to .xcprivacy reason codes and head off the ITMS-91053 rejection email before you submit
✦A Claude Code routine that re-scans on every release so declaration gaps never creep back in
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
First, detect the relevant symbols across the Swift files in your project. I asked Claude Code to "write a scanner that detects the target APIs sorted into four categories, with file and line number," then refined the draft into this.
#!/usr/bin/env python3"""scan_required_reason.py — find Required Reason APIs in your own code."""import reimport sysfrom pathlib import Path# category -> (detection symbols, candidate reason codes indie apps use)CATEGORIES = { "UserDefaults": ( [r"\bUserDefaults\b", r"\bNSUserDefaults\b"], ["CA92.1"], ), "FileTimestamp": ( [r"\.modificationDate\b", r"\.creationDate\b", r"\bcontentModificationDateKey\b", r"\bfstat\b", r"\bstat\("], ["C617.1", "3B52.1", "0A2A.1"], ), "DiskSpace": ( [r"volumeAvailableCapacity\w*Key", r"\bsystemFreeSize\b", r"\bvolumeTotalCapacityKey\b"], ["85F4.1", "E174.1"], ), "SystemBootTime": ( [r"\.systemUptime\b", r"\bmach_absolute_time\b"], ["35F9.1"], ),}def scan(root: Path): hits = {} # category -> list[(file, line_no, snippet)] for swift in root.rglob("*.swift"): if "/Pods/" in str(swift) or "/.build/" in str(swift): continue for i, line in enumerate(swift.read_text(encoding="utf-8", errors="ignore").splitlines(), 1): for cat, (patterns, _codes) in CATEGORIES.items(): if any(re.search(p, line) for p in patterns): hits.setdefault(cat, []).append( (swift.name, i, line.strip()[:80])) return hitsif __name__ == "__main__": root = Path(sys.argv[1] if len(sys.argv) > 1 else ".") found = scan(root) if not found: print("Nothing found. No self-code APIs require a declaration.") sys.exit(0) for cat, (_, codes) in CATEGORIES.items(): if cat in found: print(f"\n[{cat}] candidate reason codes: {', '.join(codes)}") for name, ln, snip in found[cat]: print(f" {name}:{ln} {snip}")
Run it as python3 scan_required_reason.py ./Sources and you get the matching lines and candidate reason codes grouped by category. On my machine it covered about 120 files and finished in roughly 0.8 seconds, surfacing exactly the three gaps I mentioned. There is far less second-guessing than tracing sources by hand.
Excluding Pods/ and .build/ is deliberate. What I want here is my own code only; dependencies carry the responsibility to declare through their own manifests. Mixing the two muddies the question of which side should file the declaration.
Turning results into .xcprivacy
Using the scan results, assemble the relevant part of PrivacyInfo.xcprivacy. A minimal example declaring UserDefaults and file timestamp looks like this.
What matters here is not filling reason codes with whatever looks safe. The UserDefaults CA92.1, for instance, carries the condition that access happens only within your app or App Group container and is not shared with third parties. If you hand values to an analytics SDK, that reason does not match reality. Keep the line clear: the script offers candidates, not conclusions.
Before / After — living with the rejection email
The old me went hunting for the offending call only after a rejection arrived. The error message (ITMS-91053) tells you which category is missing, but not where in the code. I would open and close files at random, over and over.
Before (rejection-driven)
Now (scan before submitting)
Trigger to notice
Rejection email from App Store
Local scan before submission
Locating the call
Open every file on a hunch
Pinpointed by file and line number
Round trip to resubmit
A one-to-two business-day wait
Resolved before submitting, zero round trips
Once the scan became a pre-submission step, that round trip disappeared. Moving from a reactive stance — waiting on the rejection to act — to checking things myself before submitting was a small change, but the little knot of tension I felt on every release loosened.
Handing the diff check to Claude Code
Do not stop at a one-time inventory; re-scan on every release. New code that touches another Required Reason API leaves another gap. I give Claude Code this role:
Run scan_required_reason.py and collect the list of detected categories
Read the categories already present in the current PrivacyInfo.xcprivacy
Point out, as a diff, the categories that were detected but not declared
For each diff, summarize the candidate reason codes and the matching lines
Writing these four steps into CLAUDE.md means a single "check the Required Reason APIs" runs the same inspection at the same fidelity every time. Leave only the judgment-bound choice of reason code to a human, and delegate the mechanically decidable detection and diffing. That boundary is what makes this sustainable for a solo developer.
If you fold this into CI, returning a failing exit code when an undeclared category appears is a sturdy design. The idea sits alongside my notes on continuous iOS updates in the Xcode iOS development workflow. When a dependency migration is involved, reading it together with notes on migrating from CocoaPods to SPM will give you the full manifest picture.
Points where you may stumble
A few cautions surfaced in practice. One is false positives on generic C function names like stat, which can match in strings or unrelated contexts, so always eyeball the detected lines. Another is that an App Extension or Widget is a separate target with its own .xcprivacy. Tidy up the main app alone and, if the extension is missing a declaration, you take the same rejection. Include the extension's sources in the scan root to stay safe.
When you are unsure which reason code to pick, there are cases where you can list several that genuinely match rather than forcing a single one. Still, "declare extras to be safe" is a habit to avoid. Listing reasons you do not actually use strays from the point of the declaration. I prefer to verify, one entry at a time, whether the code really uses the API for that purpose.
After you finish reading
Start by running scan_required_reason.py once against your project. If nothing comes up, that is reassurance; if something does, you have just cut one future rejection email. Reconcile the scan output against your current .xcprivacy, and for any undeclared category, weigh the reason codes one entry at a time — beginning from that first step is the right move.
I could not fully prevent declaration gaps myself until I put this in place. The rules will keep changing, and I am still keeping pace with them. If this helps a fellow solo developer take one steadier breath before submitting, I would be glad. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.