I'm just a developer, not a GDPR expert... but
A Claude Code skill checks Flutter code for GDPR compliance before every merge. What it found — and what I did about it.
I’m just a developer, not a GDPR expert… but
TL;DR: A custom Claude Code skill checks Flutter code for GDPR compliance before every merge. Results: Sentry was sending PII to US servers, the consent implementation only worked on iOS, consent state lived in a global variable, and Apple’s ATT was unnecessary because no SDK actually used the IDFA. Here’s the skill, the findings, and the code improvements.
This post assumes Flutter experience (BLoC, freezed) and a basic understanding of the GDPR (EU Regulation 2016/679). No legal expertise required. That’s kind of the whole point.
The problem
Freelance, health app, German client, Flutter, B2C, pre-launch, health data, German users. High GDPR complexity.
The consent implementation consisted of a single action: Apple’s ATT prompt on iOS. Android users got nothing. The ATT result wasn’t properly persisted. It existed as a global variable in globals.dart. On top of that, Sentry was running with sendDefaultPii: true and attachScreenshot: true, actively sending user data and screenshots of health screens to US servers.
None of this was intentional. It accumulated over sprints, the way these things do. Various tracking SDKs and forgotten debug flags.
As the developer shipping this code, I needed a systematic approach to identify privacy issues. Not as a one-off, but on every branch.
The approach: a GDPR audit skill for Claude Code
The idea: a structured prompt file that teaches Claude Code to systematically check source code against the GDPR (EU Regulation 2016/679). Instead of manually reviewing every branch for PII fields, consent gaps, and SDK configurations, an automated AI-driven process handles it, with references to specific legal articles.
Claude Code has “skills,” prompt files that give the AI domain-specific knowledge for particular tasks. A skill called /gdpr-audit turns Claude into a systematic GDPR reviewer.
Instead of giving generic recommendations about consent flows, the skill systematically examines the entire codebase.
First, it inventories all data flows: PII fields, auth tokens, device IDs, health-related terms, analytics events, error reporting configurations. All config files get checked.
Then findings are mapped to specific GDPR articles:
| Area | GDPR Article | What’s checked |
|---|---|---|
| Consent | Art. 6, 7, 9 | Granularity? Revocability? Cross-platform? Before processing? |
| Data subject rights | Art. 15-22 | Access, rectification, deletion, export possible? |
| Security | Art. 32 | PII in logs? Tokens in secure storage? Screenshots with health data? |
| Data minimization | Art. 5(1)(c)(e) | Unnecessary permissions? Retention periods? |
| Data processing | Art. 28, 44-49 | DPAs in place? Transfer mechanisms for US-based SDKs? |
| Push notifications | — | Tokens deregistered after deletion? PII in payloads? |
The output: file paths, line numbers, GDPR article references with severity level, risk assessment, and concrete remediation suggestions per finding.
The skill definition is about 195 lines of Markdown. At its core, a checklist that nobody would realistically go through manually on every branch.
What it found
Ran /gdpr-audit on the feature branch. Several critical findings.
Sentry as a PII pipeline
// Before - lib/main.dart
options.sendDefaultPii = true;
options.attachScreenshot = true;
options.screenshotQuality = SentryScreenshotQuality.low;
options.attachViewHierarchy = true;
options.maxRequestBodySize = MaxRequestBodySize.always; The audit flagged this under Article 32 (security of processing) and Article 9 (special categories). Screenshots containing health data going to Sentry’s US infrastructure. Request bodies with auth tokens. View hierarchy exposing widget trees that contained user data.
The fix:
// After - lib/main.dart
options.sendDefaultPii = false;
options.attachScreenshot = false;
options.maxRequestBodySize = MaxRequestBodySize.never;
options.beforeSend = _stripSensitiveData; A beforeSend callback redacts consent parameters, subscription keys, and access tokens from breadcrumbs:
SentryEvent? _stripSensitiveData(SentryEvent event, Hint hint) {
final breadcrumbs = event.breadcrumbs?.map((b) {
if (b.data != null && b.data!.containsKey('url')) {
final url = b.data!['url']?.toString() ?? '';
if (url.contains('b2c_cmp_') ||
url.contains('Subscription-Key') ||
url.contains('access_token')) {
final sanitized = Map<String, dynamic>.from(b.data!);
final uri = Uri.tryParse(url);
sanitized['url'] = uri != null
? '${uri.scheme}://${uri.host}${uri.path}?[REDACTED]'
: '[REDACTED]';
return Breadcrumb(
message: b.message,
category: b.category,
type: b.type,
data: sanitized,
level: b.level,
timestamp: b.timestamp,
);
}
}
return b;
}).toList();
event.breadcrumbs = breadcrumbs;
return event;
} Consent only on iOS, plus an unnecessary dependency
The original implementation called AppTrackingTransparency.requestTrackingAuthorization() directly in main(), before anything was rendered. Android users never saw a consent dialog.
The audit cited Article 7 (conditions for consent): consent must be freely given, specific, informed, and cross-platform. But the data flow analysis revealed something else: no SDK in the app used the IDFA. No ad network, no attribution service, nothing. The ATT prompt was a leftover from an earlier planning phase.
ATT was removed entirely. Not just the code, but also the package from pubspec.yaml, the NSUserTrackingUsageDescription from Info.plist, and the iOS platform check in the Cubit. Without the systematic data flow analysis, this probably wouldn’t have been caught. ATT was just there, so it was being used.
A dedicated, cross-platform consent mechanism replaced it: a ConsentCubit with a bottom sheet on first launch, on both platforms.
abstract class ConsentState with _$ConsentState {
const ConsentState._();
const factory ConsentState({
required int prefs,
required int stats,
required int market,
required bool collected,
}) = _ConsentState;
bool get isGranted => prefs == 1 && stats == 1 && market == 1;
factory ConsentState.granted() => const ConsentState(
prefs: 1, stats: 1, market: 1, collected: true);
factory ConsentState.denied() => const ConsentState(
prefs: 0, stats: 0, market: 0, collected: true);
} The consent state is persisted in SharedPreferences, scoped via BlocProvider, and validated before login:
Future<void> _collectConsentIfNeeded() async {
final consentCubit = context.read<ConsentCubit>();
if (consentCubit.state.collected) return;
final choice = await showConsentBottomSheet(context);
if (!mounted) return;
if (choice == ConsentChoice.acceptAll) {
await consentCubit.acceptAll();
} else {
await consentCubit.onlyNecessary();
}
} acceptAll sets the state directly. No more if (_isIOS) conditions, no _requestAttAndEmit() calls, no platform switching. The entire ATT logic is gone, and the Cubit shrunk to roughly half its size.
The global variable
Originally, consent parameters lived in a global variable in globals.dart. The audit identified this as both a correctness and security issue: any code could read or modify consent state.
Now everything goes through the Cubit. Consent parameters are appended to WebView URLs via explicit function parameters, not global state.
What else the audit found, and what I’ve fixed since
The most valuable part of the audit is what it identifies as missing. A high-severity finding: no revocation mechanism (Article 7, paragraph 3). Once consent was given, the only way to change it was reinstalling the app. The regulation says: “It shall be as easy to withdraw as to give consent.”
That’s fixed now. The privacy settings include a “Cookies & Tracking” toggle that runs through the existing ConsentCubit:
Future<void> _onConsentToggle(bool value) async {
final cubit = context.read<ConsentCubit>();
if (!value) {
await cubit.onlyNecessary();
return;
}
final choice = await showConsentBottomSheet(context);
if (!mounted) return;
if (choice == ConsentChoice.acceptAll) {
await cubit.acceptAll();
} else {
await cubit.onlyNecessary();
}
} Disabling is instant. Re-enabling opens the consent sheet for explicit re-consent, same as on first launch.
One high-severity finding is still open: consent bundling (Article 7). Users accept or reject everything together. Individual control over preferences, statistics, or marketing is missing. The data model already has separate fields (prefs, stats, market), but the UI doesn’t have individual toggles yet. It’s in the backlog with article references.
How I use this day-to-day
- Feature branch with changes to analytics, user data, third-party SDKs, or consent
- Run
/gdpr-auditin Claude Code before opening the pull request - Get a structured report with findings, severity levels, and file:line references
- Fix what fits in the current sprint; document the rest
The skill also flags dependency upgrades as potential risks. When upgrading sentry_flutter 8.x to 9.x, firebase_analytics 11.x to 12.x, and flutter_secure_storage 9.x to 10.x, all three were flagged under Article 28 (data processing agreements), with a recommendation to review changelogs for privacy-relevant changes. These details slip through easily when you’re doing build fixes.
What this is and what it isn’t
This doesn’t replace legal counsel. It doesn’t draft data processing agreements. It doesn’t understand contractual obligations or what the data protection officer signed off on.
What it does: run a 195-line checklist based on actual regulation against code diffs, automatically. It finds mechanical problems: the Sentry flag still set to true, the platform without a consent dialog, consent state in a global variable.
For a freelancer working on a health app in Germany, the difference between “I checked manually, should be fine” and a documented audit report with concrete findings matters. The consent implementation was about 200 lines of Dart. Sentry hardening removed code, it didn’t add any. The ATT dependency? Eliminated entirely, including the package, the Info.plist entry, and the Cubit platform check. Less code, fewer dependencies, smaller attack surface. None of it was hard once someone (or something) points at the specific line and says “Article 32, problem.” The knowledge wasn’t the bottleneck. The attention was.
FAQ
Does an AI audit replace a legal review by a lawyer?
No. The skill finds technical issues: wrong Sentry flags, missing consent dialogs, PII in logs. It can’t evaluate data processing agreements, interpret specific legal obligations, or perform a Data Protection Impact Assessment under Article 35 GDPR. It surfaces code problems. Whether those are legally relevant is for others to decide.
Why isn’t Apple’s ATT (App Tracking Transparency) enough for GDPR consent?
ATT only covers IDFA usage (Identifier for Advertisers). It’s an iOS-only framework with no Android equivalent. It doesn’t distinguish between statistics and marketing. It doesn’t meet what Article 7 GDPR describes as “freely given, specific, and informed.” GDPR compliance requires a separate consent mechanism that works on both platforms. In this case, no SDK was even using the IDFA, so ATT was completely unnecessary.
Which GDPR articles are most relevant for app developers?
Articles 6 and 7 (legal basis and consent), Article 9 (special categories, including health data), Article 17 (right to erasure), Article 28 (data processing, which covers every third-party SDK), and Article 32 (security of processing, which covers logging, error reporting, and storage).
Does the audit skill only work for Flutter?
The /gdpr-audit skill checks code patterns, not framework-specific APIs. The checklist applies to any mobile or web application. PII fields, tokens, and analytics events are framework-agnostic. Only file names need adjusting (e.g., pubspec.yaml vs. package.json).
Further reading
- GDPR full text on EUR-Lex — for looking up Article 7 or 32
- Article 7 GDPR on dsgvo-gesetz.de — more readable than the original text, with recitals
- Article 32 GDPR on dsgvo-gesetz.de — technical and organizational measures
- Sentry Privacy & Compliance — Sentry’s own privacy documentation (read before enabling
sendDefaultPii) - Claude Code — the CLI tool that runs the audit skill
The /gdpr-audit skill and the consent implementation come from a real production app. Audit reports, code examples, and open findings are from the actual branch diff.