Skip to main content

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:line evidence
  • Comments and string literals are blanked before matching, so a symbol named in a comment is never treated as evidence
  • Generic names such as creationDate carry low confidence and warn rather than error, because a linter that cries wolf gets uninstalled
  • Ambiguous symbols are reported, never guessed: Apple lists getattrlist, fgetattrlist and getattrlistat under both FileTimestamp and DiskSpace

Your dependencies

  • Detects SDKs from Pods/, Carthage/, *.xcframework, Podfile, Package.resolved and 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

xcprivacy-0.1.0.tar.gz (73.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

xcprivacy-0.1.0-py3-none-any.whl (47.5 kB view details)

Uploaded Python 3

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

Hashes for xcprivacy-0.1.0.tar.gz
Algorithm Hash digest
SHA256 34c76628337e16d9866ec5128d1a2bad87e9e859ffa2b7b5ecd582265caab935
MD5 81fa29bbfc762f6f99f5f540120e1156
BLAKE2b-256 43522f4defb103cf3efe172612dfb47dd4adc9089ab29bfc3e6c721d051e0ed5

See more details on using hashes here.

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

Hashes for xcprivacy-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5f0aeb3877a02277c7d8b99fe3ad5f99a69d4afd532b7ba06352f50bd4bec1ec
MD5 d710793010de5111678145f9c2b18bea
BLAKE2b-256 0120a9dea14e9944313f3ad2af59c44885912bf6d2dc14b6e1ef66f6cb3740e6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page