dsgvo-audit v1.0.0 law stand 2026-09 DSGVO · TDDDG · DDG MIT

How dsgvo-audit works

A Claude skill that audits a website the way a careful code reviewer would: it collects evidence before forming an opinion, follows every piece of personal data from the browser to the last third party, grades each finding against German privacy law, and only then writes the Datenschutzerklärung. This page shows how the package is split up and what happens in each step.

1 · EVIDENCEstatic scan, runtime scan, data-flow trace
2 · INTAKEask only what code can't tell
3 · AUDIT11-section checklist, Ampel status
4 · REPORTGerman, findings with § and Aufwand
5 · REMEDIATIONtemplates, gating, re-audit
01 · Anatomy

One skill, ten files, loaded in layers

Claude never reads the whole package at once. The description is always in context and decides whether the skill fires. SKILL.md loads when it does and holds the workflow. Everything else is pulled in only by the phase that needs it, so a 100 KB package costs a few kilobytes of context until the audit actually starts. Click a file to see when it is read and by which phase.

Layer 0 · always description (~150 words) decides triggering scoped: review, compliance question, adding a service triggers Layer 1 · on trigger SKILL.md (~170 lines) stance, five phases, report format, deployment rules with legal nuance per phase Layer 2 · on demand references/ · assets/ · scripts/ read when a phase points at them; the scanner is executed, never read; templates only in remediation context cost grows only as the audit progresses
Progressive loading: the part that is always present is small and decides triggering; the heavy legal material enters context only when a phase needs it.
skills/dsgvo-audit/
references/
assets/
scripts/
agents/
02 · Workflow

Five phases, in order, no skipping

The order is the point. A Datenschutzerklärung written before anyone knows what the site loads lists services that aren't there and misses the ones that are, which is itself a transparency defect under Art. 5(1)(a). So evidence comes first, questions second, and the policy last. Click a phase, or let it play.

Phase 5 runs only when the user asks for fixes, not for an audit.
03 · Evidence

Four levels of evidence, and the report says which one it had

What Claude can see depends on what it was given. Each level down loses visibility, and the report states the level reached so the client knows what was verified and what was inferred. The skill refuses to produce a generic policy from a description alone; it asks the intake questions instead. A scan that cannot reach the site returns no verdict (exit code 3), so the report drops one rung and says so; a network failure is never read as a clean site.

1a · Codebase + 1b · runtime scan deps, source, config, env names; live origins sees: third-party origins, storage, handlers, DB, processors, cookies incl. Set-Cookie, what the CMP blocks 1b · Live URL only (scanner) no repo — browser tools as fallback sees the browser side fully · blind to server handlers, DB region, AVVs Fetch HTML only script/iframe/preconnect tags, /impressum static tags only · misses runtime-injected tags, Set-Cookie, CMP behaviour Description only "a Next.js site for a physio practice" sees nothing → intake questions, never a generic template (Art. 5(1)(a))
The evidence ladder. Bars show how much of the data flow each level can verify; the report's Management Summary names the level in one sentence.

Static scan

Dependencies first (package.json is the fastest, most reliable evidence), then source for third-party origins and device storage, then server-side code for where PII leaves the app, then config for regions and headers. Env files are scanned for key names only; values never reach a report.

What grep can't see

Google Tag Manager is a black box until tags fire, so the skill asks for a container export. A fetch can't see cookies set by API responses. A language switch means every language needs its own DSE (Art. 12). Each of these is an explicit step, not a hope.

Recorded per data flow

What (data) · from where (origin, country) · when (before or after consent) · why (purpose) · legal basis · which contract (AVV, joint controllership, none) · retention. Seven fields, filled for every flow, become the report's data-flow table.

Numbered evidence

build-evidence.mjs gives every observation an ID. Each 🔴/🟠 finding ends with Evidenz: E-03 · beobachtet — or abgeleitet when it goes one step beyond what was seen (GA "loads before consent" from code with no runtime scan), or Mandantenangabe when it rests on what the client said.

Every recipient, with its role

list-processors.mjs turns the same evidence into the processor table: Art. 28, Art. 26 or independent controller, contract, region, transfer mechanism, and the list of AVVs to request. Signed or not stays ⚪️ until the client confirms; hoster and mailbox are added by hand.

What changed since last time

diff-scans.mjs compares two scans. A new tracker before consent is a 🔴 regression and fails --strict; a removed service is listed so it leaves the DSE too. dsgvo-watch.yml runs it weekly against a committed baseline.

04 · Data flow

A contact form is a pipeline with four recipients

Earlier drafts audited the page. The skill audits the path. Personal data enters in the browser and moves through edge code, API handlers, a database and a set of third parties before it lands in logs and backups. Every hop is a place where a legal basis, a contract or a retention period can be missing. Click a hop to see what is checked there.

BrowserHop 0 Edge / MWHop 1 API / ActionsHop 2 DatabaseHop 3 Third partiesHop 4 Logs · CIHop 5 PeopleHop 6 example · Kontaktformular name, e-mail, message fra1 · no cookie sendContact() logs body? rate limit? contact_requests eu-central-1 · RLS deletion cron 6 months Resend (US) · Sentry AVV signed? tracking off? Vercel logs 30 d log drain = processor inbox, dashboard MFA · Art. 15 in 30 d one form → Supabase, Resend, Sentry, Vercel logs: four recipients the Datenschutzerklärung must name (Art. 13(1)(e)), each with its own AVV and retention
The seven hops from references/architecture.md. The moving dot is one submission of a contact form; the row beneath shows what each hop looks like for that form and the question the audit asks there.
05 · Runtime scanner

Three states of the same page

scripts/scan-origins.mjs opens the site in a fresh Chromium profile and records what the browser does before anyone touches the banner. Then it clicks "Ablehnen" and checks whether anything new loads. Then it reloads, clicks "Akzeptieren", and takes an inventory of everything the site can load, which is the list the Datenschutzerklärung has to cover. In that last step it looks for an explicit accept-all button first and never clicks a reject-equivalent such as "Nur notwendige akzeptieren", so the inventory is complete even when the banner lists the reject option first. Replay the output on either fixture that ships in the repo.

fresh Chromium profile · de-DE · Europe/Berlin no cookies, no storage, no prior consent A · load + settle + scroll → record requests (origin, type), Set-Cookie headers, cookies, local/sessionStorage links: Impressum, Datenschutz, stale ODR link · GET /impressum, /datenschutz B · click "Ablehnen" DE/EN regex · roles first diff vs. A new origins after reject = 🔴 C · reload, "Akzeptieren" accept-all first · never a reject match JSON + summary --strict → exit 1 · no page → exit 3 blind spots stated: GTM container contents, subpages, server side
The scanner's three states. State A alone answers the § 25 TDDDG question; state B catches CMPs that ignore rejection; state C produces the service list for Art. 13. If the page never loads there is no state A, and the scanner says so instead of printing an empty pass.
0ok1--strict violation2usage / no Playwright3page not loaded, no verdict
06 · Verdicts

Five statuses, and an honest line between law and rule

Every checklist item gets one of five statuses. Some of the skill's rules are stricter than the law requires, because for a small practice or care service the documentation burden of the lawful-but-complicated path rarely pays off. Those rules stay 🔴 in the report, but the report says they are the agency's rule, not the statute, so the client and their lawyer can decide. Toggle the example to see the difference in wording.

StatusMeaningExample
KritischUnlawful with Abmahn-, Bußgeld- or damages risk. Blocks go-live.GA firing pre-consent · Google Fonts from CDN · no Impressum
HochClear defect, fix this sprint.No reject on first layer · missing Art. 13 items · stale OS-Plattform link · PII in logs
MittelDefensible but weak, or a conservative-rule breach.Vague retention · no VVT · US analytics with consent
OKConforms.Self-hosted fonts · AVV filed · EU region pinned
UnklarNeeds client input or runtime verification. Says exactly what.GTM without container export · DPF status of an entity

Deployment rules with their nuance. No consent → no third-party execution (law; essentials like the CMP or a payment iframe are exempt under § 25 Abs. 2 Nr. 2 TDDDG). Fonts and CDN assets self-hosted (law; consent would cure it but nobody consents to a font). Legitimate interest doesn't cure § 25 (law). Art. 9 data → no US services in the data path (rule). AVV before go-live (law). US transfers name their mechanism, DPF only for the listed entity (law). AI APIs are processors and need labelling under Art. 50 KI-VO (law). Never copy a policy (Urheberrecht). Never soften a finding, but always say which kind it is.

07 · Versioning & distribution

One tag, two audiences

The repo is a Claude Code plugin. Pushing a version tag builds the .skill package and publishes a GitHub Release, so Claude Code users update with one command and Claude.ai or Cowork users download a file. Every release also records a law stand, the month up to which the legal references were verified, because the law moves and a compliance tool must say how old its knowledge is.

npm run version:bump 1.1.0 --law-stand 2026-11 syncs 4 files + Stand CHANGELOG + commit git tag v1.1.0 npm test must be green release.yml tag == plugin.json version? validate → package → release Claude Code · plugin marketplace /plugin update dsgvo-audit@lxhelili skill + subagent, detected via version in plugin.json Claude.ai / Cowork · GitHub Release dsgvo-audit.skill (skill + agent bundled) upload under Settings → Skills semver: major = workflow/report/verdict changes · minor = new services, items, patterns · patch = corrections law stand ≠ version: a patch can move the stand; the stand tells the reader how old the legal knowledge is
From a version bump to both audiences. The tag must equal plugin.json's version or the workflow refuses, and the package step re-runs the validator so a broken skill is never shipped.

Four places carry the version; one command keeps them equal

plugin.json"version": "1.0.0"what Claude Code compares
marketplace.json"version": "1.0.0"catalog entry
package.json"version": "1.0.0"tooling
SKILL.mdmetadata.version · law-standtravels inside the .skill

npm run validate fails when they drift, when the CHANGELOG lacks the section, when evals/evals.json points at a fixture that does not exist, or when a known earlier mistake reappears (brace-glob grep, § 7 Nr. 3 UWG, an OS-Plattform link without a removal note). It warns when the description comes within 40 characters of the 1024-character limit.

08 · Evals

Tests prove the tools; evals grade the auditor

npm test shows that the grep block finds things and the scanner clicks the right button. It says nothing about whether an audit written with the skill is any good: whether it finds the GA4 tag in the layout, calls Formspree a rule-based 🔴 rather than a legal one, or asks for the Kammer instead of inventing one. Evals measure that. Each one is an invented fixture, a prompt phrased the way a developer would type it, and a list of expectations a lazy run would fail. Run with the skill and without it, the difference in pass rate is what the skill is worth; run against the previous version, the difference is what an edit did.

Eval evals/files/<fixture> prompt as a developer types it 8–11 expectations Run with skill subagent, skill loaded Baseline no skill, or the previous version Grading LLM grader: each expectation passed / failed, with evidence grade-report.mjs: structure, RDG, placeholders, § 5 DDG Benchmark with skill 46 / 47 no skill 35 / 47 pass rate · time · tokens iteration 1 (1.1.0 vs. no skill); later iterations compare each release with the previous one — evals/results/
One iteration of skill-creator's loop. The same fixture and prompt run twice; a grader agent judges the substance against the expectations, a script judges the structure, and the benchmark puts the two configurations side by side.

Five output evals, each built to catch one way of doing it wrong

EvalFixtureWhat it catches
nextjs-ga-fonts-auditNext.js: GA4 before consent, Google Fonts from the CDN, contact route to Resend + Sentry with the body in extra, regions: ["iad1"], stale ODR linkEvidence-first from code only; the hop trace (one form, four recipients); the evidence-level sentence; audit only when audit was asked
static-site-runtime-scanStatic site with "Nur notwendige akzeptieren" listed before "Alle akzeptieren"That the scanner is actually run, not replaced by reading HTML; that the reject-equivalent is not mistaken for accept; a 404 behind an existing Impressum link is 🔴
praxis-art9-intakeAesthetic-surgery booking page: treatment select, Beschwerden/Medikamente textarea, photo upload, Formspree, CalendlyArt. 9 recognised from the fields; US services in the health path as a rule-based 🔴 with the lawyer-exception note; § 203 StGB, DSFA, DSB raised; intake questions instead of a generic policy
astro-minimal-dse-generationAstro on Hetzner, @fontsource, Plausible, nodemailer to the own mailbox, a full INTAKE.mdModule discipline: only what the evidence supports; Plausible on 6(1)(f), not consent; retention from intake; no placeholder left; Art. 21 highlighted; LDI NRW named
ai-chatbot-processorOpenAI chat route with a triage system prompt, unlimited chat_logs in Supabase, UI text "Unser Team antwortet sofort"LLM as Auftragsverarbeiter (DPA, no training, retention, region); Art. 50 KI-VO labelling; Art. 22 for the triage; tax input as Art. 9-adjacent; "verify" for the DPF status

Trigger evals

Twenty prompts in German and English, twelve that should open the skill and eight that should not (a Supabase table, a Vercel build error, SPF for Resend). They test the description alone and protect its last sentence, the one that keeps ordinary dev work from turning into an audit.

Deterministic grader

scripts/grade-report.mjs checks what needs no judgment: eight sections in order, every 🔴/🟠 block with Befund, Rechtsgrundlage, Risiko, Maßnahme and Aufwand, every 🔴/🟠 finding with an evidence ID and level, every cited ID listed in the Evidenzverzeichnis, the RDG disclaimer, no "abmahnsicher", no leftover placeholder, § 5 DDG rather than TMG. Golden and broken fixtures keep the grader itself honest in CI.

When to run

Any edit to SKILL.md, references or templates: output evals against the previous version. Any edit to the description: trigger evals, three runs per prompt. A law-stand bump: output evals, and a new expectation for every changed verdict. A release: all three, with the numbers in the changelog.

What evals cannot tell you. They measure against the fixtures and expectations someone wrote. A legal position that is wrong in recht.md and repeated in an expectation passes with full marks. The primary-source rule in CONTRIBUTING is the check on the expectations; the evals check that the skill applies what the references say.

09 · Extending

Four kinds of contribution, each with a home and a guard

The parts that need many eyes are the ones that go stale: service verdicts, legal references and framework patterns. Each has a file it belongs in, an issue template that asks for the source it needs, and a test that catches the most likely way to break it. The fourth kind, an eval, is how a change to any of the other three gets measured.

Legal update

A ruling, a law change, new guidance. Goes into recht.md, and into checklist.md or services.md when it changes a verdict. Needs a primary source with a date. Bumps the law stand.

Guard: validator rejects known-stale citations; PR checklist asks for the source.

Service entry

A row in services.md (consent? basis? region? verdict + alternative), an origin regex in the scanner's SIGNATURES list so it gets labelled, and a DSE module if the generic one doesn't fit.

Guard: scanner test proves detection on a fixture page; vendor docs required.

Pattern

A consent-gating or self-hosting example for a framework that isn't covered, added under the seven generic principles in patterns.md. Principles stay framework-agnostic; examples illustrate them.

Guard: it must gate the load, not the use; anti-patterns list is the checklist.

Eval

A small invented fixture with every signal on purpose, a prompt as a developer would type it, and 8–11 expectations in evals/evals.json that a lazy run fails. A new verdict or legal position gets one, so a regression would show.

Guard: validator checks fixtures exist and ids are unique; the run compares against the previous version.

What npm test runs

CheckWhat it protectsHow
validate.mjsPackage integrity, version sync, frontmatter, dangling references, agent definition, eval definitionsStatic checks, no dependencies
phase1.test.shThe Phase 1 commands actually find thingsExtracts the real bash block from SKILL.md, runs it on a fixture repo, asserts hits and that no secret value is printed
grade-report.test.mjsThe deterministic grader itselfAccepts the golden report, policy and Impressum; rejects the broken report for seven named reasons
scanner.test.mjsThe runtime scanner end to endThree cases: the non-compliant page (pre-consent origins, reject diff, accept inventory, stale ODR link), the banner with the reject option first, and an unreachable URL that must exit 3 without a verdict
packageThe .skill is buildable and validatedRuns in CI on every push; artifact kept 14 days