xcprivacy
Find out your PrivacyInfo.xcprivacy is wrong now — not after the upload, from a rejection email.
Apple rejects App Store builds over one wrong string in PrivacyInfo.xcprivacy.
plutil -lint passes it. Xcode says nothing. You find out after building and
uploading, usually on release day.
xcprivacy catches it first — with no Xcode, no macOS, and zero dependencies,
so it runs on a Linux CI runner.
$ pip install xcprivacy
$ xcprivacy
PhotoVault/PrivacyInfo.xcprivacy
error XCP103 tracking domain 'https://sdk.adnetwork.io/v2/track' includes a scheme
predicts ITMS-91056
Apple requires a bare host such as 'analytics.example.com' - no scheme, no path, no
query, no trailing slash.
error XCP208 NSPrivacyAccessedAPITypes[0] declares reason 35F9.1 under
NSPrivacyAccessedAPICategoryFileTimestamp, but 35F9.1 belongs to
NSPrivacyAccessedAPICategorySystemBootTime
predicts ITMS-91055
Allowed reasons for NSPrivacyAccessedAPICategoryFileTimestamp: DDA9.1, C617.1, 3B52.1,
0A2A.1. If you meant to declare 35F9.1, move it to a
NSPrivacyAccessedAPICategorySystemBootTime entry.
error XCP209 NSPrivacyAccessedAPITypes[1] declares C56D.1, which Apple restricts to third-party SDKs
predicts ITMS-91055
Apple: "Third-party SDK wrapper around user defaults APIs. SDKs only." If this manifest
belongs to an app rather than an SDK, choose one of: CA92.1, 1C8F.1, AC6B.1
error XCP303 NSPrivacyCollectedDataTypes[0] has unknown NSPrivacyCollectedDataType value
'NSPrivacyCollectedDataTypePhotosOrVideos'
predicts ITMS-91056
Did you mean 'NSPrivacyCollectedDataTypePhotosorVideos'? (note Apple's spelling:
lowercase 'o' in 'or')
error XCP501 Disk space is used in 1 place(s) but is not declared: PhotoVault/LibraryScanner.swift:9
volumeAvailableCapacityForImportantUsageKey
predicts ITMS-91053
Add an NSPrivacyAccessedAPITypes entry with NSPrivacyAccessedAPIType =
NSPrivacyAccessedAPICategoryDiskSpace and a reason from: 85F4.1, E174.1, 7D9E.1, B728.1
Pods/FirebaseCrashlytics/FirebaseCrashlytics.framework
error XCP601 FirebaseCrashlytics is on Apple's required list but no PrivacyInfo.xcprivacy was found
inside FirebaseCrashlytics.framework
predicts ITMS-91061
Update this dependency to a version that ships a manifest. Apple has required one for
every listed SDK since 12 February 2025.
FAIL 6 error(s), 0 warning(s) (1 manifest(s), 1 source file(s))
likely App Store Connect rejections: ITMS-91053, ITMS-91055, ITMS-91056, ITMS-91061
The bug nobody catches
Reason codes are scoped per category. 35F9.1 is legal under
NSPrivacyAccessedAPICategorySystemBootTime and illegal under
NSPrivacyAccessedAPICategoryFileTimestamp.
A manifest with that mistake is a perfectly valid property list, so nothing on your
machine complains — and App Store Connect returns ITMS-91055.
It is not hypothetical: this exact bug shipped in Microsoft's App Center SDK
(microsoft/appcenter-sdk-apple issue 2543).
No actively maintained tool validates per-category reason scoping. This one does.
The five rejection codes it predicts
| Code | Meaning |
|---|---|
ITMS-91053 |
You use a required-reason API you did not declare |
ITMS-91054 |
Invalid API category declaration |
ITMS-91055 |
Invalid API reason declaration |
ITMS-91056 |
Valid plist, invalid keys or values |
ITMS-91061 |
A third-party SDK on Apple's list has no manifest |
What it checks
Manifest contents
- All four top-level keys, their types, and rejection of unknown keys
- Category values against Apple's 5-category enumeration
- Every reason code against the enumeration for its own category
- SDK-only reason codes (
0A2A.1,C56D.1) misused in an app manifest - All 35 data types and 6 purposes, with did-you-mean for near misses
- Tracking domains per Apple TN3181: no scheme, path, query or trailing slash
- Reads binary plists as well as XML — compiled frameworks ship binary, and those are exactly the manifests most likely to be broken
Your source
- Scans Swift / Objective-C / C for required-reason API usage and cross-checks it
against what you declared, with
file:lineevidence - Comments and string literals are blanked before matching, so a symbol named in a comment is never treated as evidence
- Generic names such as
creationDatecarry low confidence and warn rather than error, because a linter that cries wolf gets uninstalled - Ambiguous symbols are reported, never guessed: Apple lists
getattrlist,fgetattrlistandgetattrlistatunder both FileTimestamp and DiskSpace
Your dependencies
- Detects SDKs from
Pods/,Carthage/,*.xcframework,Podfile,Package.resolvedand matches them against Apple's published 86-SDK list - Flags any listed SDK bundled without a manifest
Usage
xcprivacy # check the current directory
xcprivacy path/to/MyApp # check a project
xcprivacy MyApp.app/PrivacyInfo.xcprivacy # check a single manifest
xcprivacy --strict # warnings become errors
xcprivacy --format sarif -o out.sarif # for GitHub code scanning
xcprivacy --format json # for scripting
xcprivacy rules # list all 42 rules
xcprivacy explain ITMS-91055 # you got the email; what now?
Exit codes: 0 clean, 1 errors found, 2 bad usage or I/O.
Adopting it on a legacy project
xcprivacy --write-baseline .xcprivacy-baseline.json # accept today's reality
xcprivacy --baseline .xcprivacy-baseline.json # fail only on new findings
Fingerprints deliberately exclude line numbers, so editing code above a finding does not resurrect it.
Why Python
The developers hit hardest by this are React Native, Flutter, Cordova and MAUI
teams whose CI runs on Linux — and every other tool in this space requires macOS,
xcrun, or CocoaPods. On a Linux runner they currently have nothing.
Python's standard-library plistlib reads binary and XML plists with no
dependencies, so this installs anywhere in one command and has an empty
dependency tree, which matters for a tool you run in CI over a compliance
artifact. CI asserts that the dependency list stays empty.
What it deliberately does not do
- Mach-O binary symbol analysis — the right v2 feature, but doing it properly means walking symbol tables and dyld bind opcodes across slices. Source scanning covers first-party code today, which is where you can actually act.
- Generating manifests — five tools already generate. None validate.
- Auto-fixing — a tool that silently edits your compliance artifact is a liability. It reports; you decide.
- Signature verification — needs
codesign, therefore macOS. - Anything involving an LLM — the rule set is a finite enumeration. Deterministic means unit-testable, and being provably right is the whole product.
Trusting the rule data
Every table is transcribed from Apple's documentation, retrieved 2026-08-03, with
the source recorded per table in src/xcprivacy/rules/PROVENANCE.md.
The test suite pins the counts — 5 categories, 17 reason codes, 35 data types, 6 purposes, 86 SDKs — and exhaustively asserts that all 68 illegal category/reason pairings are caught and all 17 legal ones are accepted, so the flagship check cannot silently regress.
It also preserves Apple's genuine oddity: NSPrivacyCollectedDataTypePhotosorVideos
has a lowercase o in "or". "Correcting" it would make the validator reject the
only value App Store Connect accepts.
License
MIT.
Not affiliated with or endorsed by Apple Inc. ITMS-* codes and privacy manifest
key names are Apple's; this tool only checks your files against Apple's published
rules.
Metadata
Release files for xcprivacy 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xcprivacy-0.1.0.tar.gz | 73.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xcprivacy-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 121.0 kB
Release files / xcprivacy-0.1.0.tar.gz
| Download URL | xcprivacy-0.1.0.tar.gz |
|---|---|
| Size | 73.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
34c76628337e16d9866ec5128d1a2bad87e9e859ffa2b7b5ecd582265caab935
|
|
BLAKE2b-256 checksum How to use checksums |
43522f4defb103cf3efe172612dfb47dd4adc9089ab29bfc3e6c721d051e0ed5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|
Release files / xcprivacy-0.1.0-py3-none-any.whl
| Download URL | xcprivacy-0.1.0-py3-none-any.whl |
|---|---|
| Size | 47.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5f0aeb3877a02277c7d8b99fe3ad5f99a69d4afd532b7ba06352f50bd4bec1ec
|
|
BLAKE2b-256 checksum How to use checksums |
0120a9dea14e9944313f3ad2af59c44885912bf6d2dc14b6e1ef66f6cb3740e6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.25
|