Pre-flight validator for Apple privacy manifests. Catch ITMS-91053/91055/91056 before you upload — on any OS, with no Xcode.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xcprivacy-0.1.0.tar.gz.
File metadata
- Download URL: xcprivacy-0.1.0.tar.gz
- Upload date:
- Size: 73.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34c76628337e16d9866ec5128d1a2bad87e9e859ffa2b7b5ecd582265caab935
|
|
| MD5 |
81fa29bbfc762f6f99f5f540120e1156
|
|
| BLAKE2b-256 |
43522f4defb103cf3efe172612dfb47dd4adc9089ab29bfc3e6c721d051e0ed5
|
File details
Details for the file xcprivacy-0.1.0-py3-none-any.whl.
File metadata
- Download URL: xcprivacy-0.1.0-py3-none-any.whl
- Upload date:
- Size: 47.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f0aeb3877a02277c7d8b99fe3ad5f99a69d4afd532b7ba06352f50bd4bec1ec
|
|
| MD5 |
d710793010de5111678145f9c2b18bea
|
|
| BLAKE2b-256 |
0120a9dea14e9944313f3ad2af59c44885912bf6d2dc14b6e1ef66f6cb3740e6
|