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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.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.
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.
| Status | Meaning | Example |
|---|---|---|
| Kritisch | Unlawful with Abmahn-, Bußgeld- or damages risk. Blocks go-live. | GA firing pre-consent · Google Fonts from CDN · no Impressum |
| Hoch | Clear defect, fix this sprint. | No reject on first layer · missing Art. 13 items · stale OS-Plattform link · PII in logs |
| Mittel | Defensible but weak, or a conservative-rule breach. | Vague retention · no VVT · US analytics with consent |
| OK | Conforms. | Self-hosted fonts · AVV filed · EU region pinned |
| Unklar | Needs 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.
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.
plugin.json's version or the workflow refuses, and the package step re-runs the validator so a broken skill is never shipped."version": "1.0.0"what Claude Code compares"version": "1.0.0"catalog entry"version": "1.0.0"toolingmetadata.version · law-standtravels inside the .skillnpm 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.
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 | Fixture | What it catches |
|---|---|---|
nextjs-ga-fonts-audit | Next.js: GA4 before consent, Google Fonts from the CDN, contact route to Resend + Sentry with the body in extra, regions: ["iad1"], stale ODR link | Evidence-first from code only; the hop trace (one form, four recipients); the evidence-level sentence; audit only when audit was asked |
static-site-runtime-scan | Static 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-intake | Aesthetic-surgery booking page: treatment select, Beschwerden/Medikamente textarea, photo upload, Formspree, Calendly | Art. 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-generation | Astro on Hetzner, @fontsource, Plausible, nodemailer to the own mailbox, a full INTAKE.md | Module 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-processor | OpenAI 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 |
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.
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.
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.
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.
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.
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.
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.
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.
npm test runs| Check | What it protects | How |
|---|---|---|
validate.mjs | Package integrity, version sync, frontmatter, dangling references, agent definition, eval definitions | Static checks, no dependencies |
phase1.test.sh | The Phase 1 commands actually find things | Extracts 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.mjs | The deterministic grader itself | Accepts the golden report, policy and Impressum; rejects the broken report for seven named reasons |
scanner.test.mjs | The runtime scanner end to end | Three 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 |
package | The .skill is buildable and validated | Runs in CI on every push; artifact kept 14 days |