Skip to content

How data-safety-from-code works, step by step

The skill splits the work between scripts and the agent. Scripts do everything that must come out the same on every run: reading files, matching SDKs, writing and validating the CSV. The agent does the judgement calls: following data through your code, deciding what leaves the device, and asking you what the code can’t show.

Code, then findings with file and line, then the CSV, then the Play Console import.

The examples below come from the test fixtures in the repo: a small weather app that sends the user’s coarse location to its API over plain HTTP.

scan.py walks the project, skipping build outputs, and reads:

Source What it looks for
AndroidManifest.xml (all source sets, plus the merged release manifest if you’ve built) Permissions, usesCleartextTraffic, networkSecurityConfig
build.gradle, build.gradle.kts, gradle/libs.versions.toml Dependencies, including version-catalog aliases and bundles, and their configuration (implementation ships; testImplementation and debugImplementation don’t)
./gradlew :app:dependencies output (optional) SDKs pulled in transitively
res/xml/network_security_config.xml cleartextTrafficPermitted
Kotlin and Java source 20 kinds of signal: HTTP clients, uploads, Firestore writes, analytics events, AI requests, sign-in methods, device identifiers, billing, contacts, installed apps and more
Source, res/values, Gradle Plain http:// URLs

For every permission it knows, it also checks whether your code calls the API that reads that data. In the weather app, the camera permission is never used:

{
"name": "android.permission.CAMERA",
"file": "app/src/main/AndroidManifest.xml",
"line": 5,
"types": ["PSL_PHOTOS", "PSL_VIDEOS"],
"used_in_code": false
}

And it combines the cleartext settings with the URLs it found:

"encrypted_in_transit_suggestion": false,
"reason": "plain http:// URLs found and cleartext traffic is permitted"

Each dependency is matched against knowledge/sdks.yaml. An entry lists what the vendor says the SDK collects or shares, for which purposes, and a link to the vendor’s Play data disclosure. Data the SDK always sends goes into the draft automatically. Data it sends only under a condition, such as Crashlytics with a user ID attached, goes to review so the agent can check the code for that condition. Browse the SDK entries.

The scan flags code; it doesn’t decide. The agent opens each flagged line and follows the data. Here the location goes from FusedLocationProviderClient straight into a Retrofit call, so it leaves the device. The API only uses it to answer the request, so the processing is ephemeral. The agent records that decision with its evidence:

{
"type": "PSL_APPROX_LOCATION",
"collected": true,
"shared": false,
"ephemeral": true,
"purposes": ["PSL_APP_FUNCTIONALITY"],
"reason": "Coarse location is sent to the weather API to fetch the forecast, not stored",
"evidence": ["app/src/main/java/dev/example/weather/LocationRepository.kt:12"]
}

It asks you only what code can’t show: your privacy policy URL, account creation and deletion, what your backend shares, and which data is optional for users. Google’s definitions the agent follows are in the definitions reference.

draft.py merges SDK data, the agent’s decisions and your answers into a declaration. Purposes are combined across sources; a type is ephemeral only if every source that collects it says so. fill_csv.py writes it into the template: your own Play Console export when you give one, otherwise the bundled copy. Only the Response value column changes. Rows, order, labels and line endings stay byte for byte, which is what lets Play Console import the file.

validate.py checks the file the way the form does:

  • rows and answer requirements match the template exactly
  • values are only true, false, blank or text where text is expected
  • the required question is answered; “no account creation” isn’t combined with other methods; an account deletion URL is given when users can create accounts
  • every selected data type says collected and/or shared, answers the ephemeral question, picks exactly one of required/optional, and has at least one purpose
  • data types that aren’t selected have no usage answers

report.py writes the review report. For the weather app, the part that needs a human looks like this:

## Needs your review
- `android.permission.CAMERA` is declared in `app/src/main/AndroidManifest.xml:5` but no code uses it.
If no library needs it, remove it: unused sensitive permissions are a policy risk on their own.
- HTTP client: data passed to these calls leaves the device —
`app/src/main/java/dev/example/weather/LocationRepository.kt:4` (+2 more).
- Plain HTTP URL `http://api.weather-fixture.net/v1/` at
`app/src/main/java/dev/example/weather/LocationRepository.kt:8`.

When you pass your current export, the report also lists what changed: data types added or removed, purposes, sharing and the top-level answers. See a full example report.

Built by Sanikant. Set in Google Sans Flex (SIL Open Font License), icons from Material Symbols (Apache 2.0). Google Play is a trademark of Google LLC; this project is not affiliated with Google. Output is a draft for review: you are responsible for your Data safety declaration.