Germany — CLAUDE.md
/app/countries/de/CLAUDE.md
Country OS — Germany
Unlike tn/fr, this country dir does not manage a site. It runs data-collection campaigns for the German market (project "Smart research", with Stefanie Schäffer/SEO).
Current campaign: netzbetreiber
Goal: contact data for ~850 German electricity grid operators (Stromnetzbetreiber),
to later request their §14a EnWG module prices. Files live in campaigns/netzbetreiber/:
seed.csv— Stefanie's MaStR export (source of truth for the list; key:MaStR-Nr.). Read-only: never edit this file.enrichment.jsonl— one JSON record per researched operator, append-only. A re-research appends a new record; exports take the latest record per MaStR-Nr.export_csv.py— rendersenrichment.csvfrom the JSONL for import into the team's Google Sheet.
Hard rules
Never invent contact data. A website counts only if verified official (the Impressum names the operator or its Stadtwerke parent). An email counts only if found on the operator's own site or its public MaStR record — never guessed from patterns like info@domain. If nothing verifiable is found, record
contact_status: "no_contact_found"— an honest gap beats a wrong address.Never contact anyone by hand. No form submissions, no hand-written emails, not even "test" ones. Enrichment and discovery tasks are research-only. One exception, and it is not a loophole: the netzbetreiber campaign sends §14a follow-ups through
campaigns/netzbetreiber/followups.mjs send, a deterministic script thenetzbetreiber-repliesskill runs at the end of every pass (its step 7). It only mails drafts already infollowups-proposed.jsonl, it self-skips unless the mailbox is fully triaged, and it re-checks every draft against current data before SMTP (do_not_contact / data already complete / operator replied since drafting → superseded). Running it exactly as step 7 directs is expected and required — withholding it is the deviation. What stays forbidden is composing and sending mail yourself, outside that script. (Reconciled 2026-08-24: this rule predated the sending stage, and its old "this task family never contacts anyone" wording made runs flip-flop on step 7.)Anti-bot policy (decided by Frédéric 2026-07-20): simple challenge questions (arithmetic like "addieren Sie 1 und 6", basic quiz fields) may be answered when sending a legitimate inquiry. reCAPTCHA/hCaptcha/Turnstile/Cloudflare challenges and image captchas are never solved or bypassed — those operators get the email channel or the human queue. Honeypot ("leave empty") fields must never be filled.
One exception, for documents an operator sent us (decided by Frédéric 2026-08-29). A URL that appears in an operator's own reply may be opened with a real browser engine, and the document behind it downloaded, for that operator only. This is retrieval of material handed to us in correspondence, not discovery — the operator chose the address, and the §14a figures behind it are publication-obliged under EnWG anyway. A challenge that resolves on its own when an ordinary browser loads the page is allowed to resolve. A cookie-consent banner may be answered, and is always answered by refusing everything optional — that is a truthful answer about tracking, not a click-through to make the page work.
What stays forbidden is unchanged and applies here too: no captcha is ever solved, answered, or sent to a solving service; no login or paywall is passed; no honeypot is filled; nothing is submitted; and no URL we found ourselves is fetched this way. The browser must be an ordinary one — no stealth plugin, no fingerprint patching, no solver service, and browserless's own
stealthlaunch flag stays off. Those exist and they work; using them is what turns "the page loaded normally" into "we defeated a control", which is the thing this rule is for. If a page needs any of that, it does not "resolve on its own" and the operator goes to the human queue, exactly as before.The preconditions are machine-checked in
campaigns/netzbetreiber/fetch_reply_doc.mjs, never left to a run's judgment: the URL came out of a message whose mastr_nr matches, its host is the operator's own domain or a statutory portal (enwg-veroeffentlichungen.de), the request is a plain document GET, anddo_not_contact.txtis refused outright. Fetching a link by hand bypasses all four, which is why the skill tells you to run the script rather than curl the URL yourself.Second exception, for Preisblätter published on the operator's own website (decided by Frédéric 2026-09-01). The clause above says "no URL we found ourselves is fetched this way". For this campaign's §14a price sheets, that no longer holds, and the reason is the operators' own answer: Stefanie phoned the silent ones and was told, over and over, "das finden Sie auf unserer Website". Three email rounds got no reply, the phone round got a redirect to the website, and §14a figures are publication-obliged under EnWG. So we read the website — both with a plain GET and, where a WAF stands in the way, with an ordinary browser engine.
The host rule is what keeps this from becoming general crawling, and it is unchanged: every URL fetched must sit on the operator's own domain (their verified
website, their email domain, or a brand-sharing sibling persameOperator()) or onenwg-veroeffentlichungen.de. A web search may be used to locate a Preisblatt, but the URL it returns is subject to the same check — hand it tohunt_preisblatt.mjs --mastr … --url …, which enforces it, rather than fetching it yourself. A hit on a third-party host is refused, not argued about; when the search finds nothing on their own domain, go back to the operator's site and look there.Everything the first exception forbids stays forbidden here, without exception: no captcha is solved, answered or outsourced; no login or paywall is passed; no form is submitted; no honeypot is filled; the browser stays ordinary, with browserless's
stealthflag off. Cookie banners are answered by refusing everything optional, as above. A page that does not resolve on its own goes to the human queue. What we are allowed to do is read what an operator chose to publish; we are not allowed to get past anything they put in the way.hunt_preisblatt.mjsperforms the discovery and enforces the host rule anddo_not_contact.txt; a URL it cannot turn into a document lands infetch-queue.jsonl, whichfetch_browser.mjsdrains under this same rule.Record a
sourcefor every field group so freshness re-checks are possible later.As few comments as possible in the code you ship. A comment says why, and only when the reason is genuinely unusual — something a competent reader could not reconstruct from the code itself. Never narrate what the code does, never restate a name, never gloss a self-describing constant. Test: delete the comment and re-read the code; if it still makes sense, it stays deleted. What survives looks like a workaround for one specific device or vendor quirk, or a value that looks arbitrary and is not. Exception: configuration files (PHP config,
.env.example, YAML manifests) — there a comment is the only documentation an option has, so comment freely. This governs shipped code only; PR bodies, register and journal entries are where you explain yourself at length.
Language
Operator websites are German. Search in German (Impressum, Kontakt, Netzbetreiber, Störung). Notes in the JSONL may be English.
Memory
memory/journal.md is the run journal — read it at the start of every run, append
a dated entry at the end (batch processed, hit rate, oddities worth remembering).