Public-Mirror
Ein privates Arbeits-Repo bleibt privat. Der oeffentliche Ableger ist ein
eigenes GitHub-Repo mit eigener Wurzel-History — nie ein Filter-Zweig des
privaten.
Nicht verhandelbar
- Frische History. Der Ableger startet mit eigenem Wurzel-Commit. Kein
filter-repo-Abkoemmling des privaten Repos, nie push --mirror, nie
push refs/*.
- Allowlist, nicht Blocklist. Default ist privat. Eine neu angelegte
Datei landet draussen, bis jemand sie ausdruecklich freigibt.
- Zwei Gates, eine Marker-Quelle. Inhalts-Gate vor dem Commit +
fail-closed
pre-push-Hook. Beide sourcen dieselbe Marker-Datei.
- Quelle ist
git ls-files. Nie den Worktree kopieren — sonst wandern
untracked Dateien mit.
- Read-only-Regel bei Audits. Auf einem bereits publizierten Repo nie
ungefragt
filter-repo, force-push oder gc. Rewrite ist eine
Einzelfreigabe pro Repo.
Sonderfall: Repo hat gar kein privates Elternteil
Nicht jede Veroeffentlichung ist ein Ableger. Ein Repo, das von Anfang an
allein stand und nur auf private steht, wird durch einen Sichtbarkeits-
Flip public — kein Sync-Skript, keine Allowlist, kein zweites Repo. Die
Mirror-Maschinerie waere hier Ballast; es gilt stattdessen eine
Veroeffentlichungs-Checkliste:
- Alle Leak-Klassen aus
patterns.md gegen alles scannen, was der
Server kennt — nicht gegen den Worktree: main, alle Tags, und
refs/pull/*/head. Lokale, nie gepushte Branches werden nicht public.
- LICENSE. Ohne Lizenz heisst public "alle Rechte vorbehalten" — das
Repo ist sichtbar, aber fuer Dritte unbenutzbar. Meist der eigentliche
Zweck der Veroeffentlichung und trotzdem der haeufigste Fehlbetrag.
- Steuerdateien (
STATE.md/TASK.md) untracken und aus der
History entfernen. Ein blosses git rm laesst sie in allen Alt-Commits
lesbar. Danach .gitignore + Verweise in verbleibender Doku aufloesen,
sonst zeigt sie auf Dateien, die es im Repo nicht gibt.
- Rewrite VOR dem Flip, nie danach — solange privat, ist jeder Fehler
noch folgenlos korrigierbar. Reihenfolge: Inhalt committen → Rewrite →
force-push → Remote verifizieren → dann erst umschalten.
- Restrisiko benennen: nach dem force-push bleiben die alten Commits
serverseitig unerreichbar liegen und sind per SHA noch eine Weile
abrufbar. Airtight waere nur ein frisches Repo — Preis: Stars, Issues
und PR-History. Bewusste Abwaegung des Owners, keine stille Entscheidung.
History-Strategie — Entscheidungsbaum
Wird der Ableger aus dem privaten Repo REGENERIERT?
├─ ja → Squash-Snapshot: genau 1 Commit, Folge-Syncs per
│ `git commit --amend --reset-author`
│ + `git reflog expire --expire=now --all && git gc --prune=now`
│ Vorteil: ein spaeter enger gezogener Filter wirkt RUECKWIRKEND.
│ Preis: jeder Folge-Sync ist ein force-push.
│ **`--reset-author` ist nicht optional.** `--amend` allein tauscht nur
│ den COMMITTER und laesst den Author stehen — eine spaeter umgestellte
│ `user.email` (z. B. auf `<id>+<user>@users.noreply.github.com`) wirkt
│ deshalb nie, und die alte Klarnamen-Mail bleibt in jedem Folge-Sync
│ stehen. Nach dem Sync pruefen, nicht annehmen:
│ `git log --format='%an %ae|%cn %ce' --all | sort -u`
│ **CHANGELOG ist hier PFLICHT, nicht Kuer.** Ein Snapshot hat genau
│ einen Commit — die History traegt fuer einen fremden Leser null
│ Information darueber, was sich je geaendert hat. Der CHANGELOG ist
│ das einzige Artefakt, das Entwicklung ueberhaupt sichtbar macht.
│ Gemessen 27.07.2026: drei Ableger, alle 1 Commit, zwei ohne CHANGELOG.
└─ nein → Der Ableger wird eigenstaendig weiterentwickelt (CI, Issues, PRs).
Organische History ist dann richtig — aber sie ist UNWIDERRUFLICH:
alles muss beim ersten Push stimmen. Pflicht: Guard + CHANGELOG.
NIE: akkumulierende Snapshot-History (jeder Sync ein Commit obendrauf, Inhalt
komplett neu erzeugt). Das ist die schlechteste Kombination — der Sync denkt
in "aktueller Stand", die History haelt jeden frueheren fest. Realfall: ein
Wohnort stand 151x in zwei Snapshots; der Scrub kam einen Tag spaeter und
konnte nichts mehr retten.
A) Neuen Ableger anlegen
- Inventur.
git -C <priv> ls-files | wc -l. Jede Datei genau einer
Klasse zuordnen: public, privat, gescrubbter Override.
tools/sync-public.sh anlegen mit ist_privat() als case —
Default-Arm *) return 0 ;; (= privat). Public wird nur, was eine
explizite Zeile freigibt. Praefix-Muster (analysis/site-pool*), nicht
exakte Pfade: Varianten entstehen spaeter.
- Overrides nach
tools/public/ (README, LICENSE, .env.example,
Beispiel-Configs). Achtung: Overrides sind selbst eine Leak-Quelle —
sie muessen durch dasselbe Gate wie alles andere (siehe C).
- Entkopplung statt Divergenz. Fehlende private Module ueber
importlib.util.find_spec erkennen und Features gar nicht erst
registrieren — besser als kaputte Knoepfe oder ein zweiter Code-Zweig.
tools/leak_markers.sh als einzige Marker-Quelle anlegen
(Muster: patterns.md nebenan).
- Sync laufen lassen, Gate muss gruen sein, Tests im Snapshot gruen.
- Repo zuerst privat anlegen, pushen, Guard verifizieren, Inhalt
durchsehen — dann auf public schalten.
B) Bestehenden Ableger syncen
cd <privat> && git status --porcelain # sauber?
tools/sync-public.sh # Gate bricht VOR dem Commit ab
cd <public> && git push # Guard laeuft als pre-push
- Erst
git add, dann syncen. Quelle ist git ls-files — eine neue,
noch nicht geaddete Datei ist fuer den Sync unsichtbar und fehlt im
Snapshot, ohne Warnung.
Source: <sha> in die Commit-Message. Ohne diesen Trailer ist der
Drift nicht bestimmbar. Damit:
git -C <priv> rev-list --count <sha>..HEAD
Ein privates commit --amend am Quell-Commit verwaist den Trailer. Der
Source:-SHA im Ableger zeigt danach auf einen Commit, den es nicht mehr gibt
— und der naechste Sync repariert das NICHT, weil der Inhalt unveraendert ist
("nichts geaendert, kein Commit"). Die Drift-Rechnung bricht dann still. Vor
jeder Drift-Aussage den Trailer-SHA auf Existenz pruefen, sonst nachziehen:
git -C <priv> cat-file -e "<trailer-sha>^{commit}" || echo "Trailer verwaist".
Realfall: ein Message-Fix per Amend, danach zeigte der Trailer ins Leere.
Der Trailer misst Commit-Distanz, nicht Content-Drift. Schreitet das
private Repo durch public-irrelevante Commits fort (privates tools/, private
Seiten), meldet rev-list --count <sha>..HEAD eine Drift, die inhaltlich
keine ist. Die verlaessliche Currency-Probe ist der No-op-Sync selbst:
laeuft er durch und meldet "nichts geaendert", ist der Ableger nachweislich
aktuell — billiger und ehrlicher als jede Trailer-Distanz. Den Trailer fuer
Nachvollziehbarkeit fuehren, die Aktualitaetsfrage mit dem Sync beantworten.
- Der Sync ist tree-gated — Message- und Identitaets-Aenderungen traegt er
nicht. Er committet nur, wenn
git diff --cached nicht leer ist. Alles, was
ausschliesslich in der Commit-Message oder in Author/Committer lebt — ein
nachgeruesteter Source:-Trailer, eine korrigierte Message, eine per
git config umgestellte Mail — wird von einem Re-Sync nie aufgegriffen,
solange der Tree gleich bleibt. Solche Aenderungen brauchen ein manuelles
git commit --amend --reset-author im Ableger plus force-push. Realfall: ein
nachgeruesteter Trailer, Sync meldete "nichts geaendert" und haette ihn nie
gelandet.
- Lokal != publiziert. Vor jeder Aussage ueber "was ist oeffentlich":
git ls-remote <url> main gegen git rev-parse main. Ein ungepushter
Commit ist noch abwendbar, ein gepushter nicht.
- Kein
--no-verify. Der Schalter kippt ALLE Pruefungen, nicht die eine,
die meldet — und wirkt am staerksten beim ersten Push. Ein unerklaerlicher
Guard-Fehler ist ein Grund abzubrechen, nicht zu umgehen.
- Snapshot-Tests im Snapshot laufen lassen, nicht aus
tools/public/. Tests,
die Konfig-/Fixture-Pfade relativ zu __file__ aufloesen, melden aus dem
Quellbaum Fehlschlaege, die wie echte Defekte aussehen — reine Diagnose-Falle.
Realfall 27.07.2026: 22 Pfad-Artefakt-Fehlschlaege, alle folgenlos. Erst
syncen, dann im Zielbaum testen.
- Erst die History-Strategie feststellen, dann syncen.
git log --oneline
im Ableger UND git ls-remote <url>: genau 1 Commit auf beiden Seiten heisst
Squash-Snapshot, und dann ist der Sync git commit --amend --reset-author
- force-push, nicht "Commit obendrauf". Wer das verwechselt, baut aus einem
Squash-Repo genau die akkumulierende Snapshot-History, die der
Entscheidungsbaum verbietet. Die Instanz-Tabelle unten ist eine KOPIE dieser
Tatsache und kann veraltet sein (Realfall 23.07.2026: sie behauptete
"organische History" fuer ein Squash-Repo) — im Zweifel gilt das Repo.
B2) Portieren statt Diffen — wenn der Ableger divergiert ist
Ein Ableger, der laenger lebt, ist irgendwann kein Spiegel mehr: uebersetzte
Bezeichner, eigene Weiterentwicklung, Features die public frueher ankamen als
privat. Dann ist git diff zwischen den Repos wertlos und ein Diff-Apply
gefaehrlich — die private Aenderung muss auf die public Struktur abgebildet
werden. Reihenfolge, die sich bewaehrt hat:
- Erst den public Ist-Stand feststellen, nicht den privaten Diff anwenden.
Was hat der Ableger von der Aenderung schon? Wo ist er eigenstaendig
weitergegangen? Realfall: eine gemeinsame Hilfsschicht war public bereits da,
aber an einer Stelle anders implementiert — ein Diff-Apply haette einen Test
gebrochen, den es privat gar nicht gibt.
- Aenderung als Spezifikation lesen, nicht als Patch — welches Verhalten,
welche Felder, welche Einfuegestelle. Das Diff-Lesen laesst sich delegieren;
die leak-sensible Anwendung nicht.
- Einfuegestelle im Ziel verifizieren, nicht raten (Nachbarzeilen lesen),
dann Feature fuer Feature portieren, jeweils mit gespiegeltem Test in der
Zielsprache des Ablegers.
- Vorbestehende Defekte im Ziel sind nicht Teil des Ports. Faellt beim
Portieren ein Bug auf, der schon vorher da war: melden, nicht im Sync-Commit
mitfixen — sonst vermischt sich Port und Reparatur, und der Commit wird
unpruefbar.
Ein Task-Ticket spannt oft Portierbares UND Nicht-Portierbares. Die
Ausschlussliste ist datei-basiert und trifft deshalb die falsche Einheit: eine
Aenderung kann eine generische Haelfte haben, die raus darf, und eine
personenbezogene, die nicht darf (Realfall: derselbe Task lieferte einen
generischen Steuer-Guard und Konto-Varianten-Strategien). Die Portier-Frage
deshalb pro Hunk stellen, nicht pro Ticket — und im Sync-Bericht
ausdruecklich benennen, welche Haelfte bewusst drinnen blieb.
C) Pre-Push-Gate — Checkliste
Fail-closed: fehlt die Marker-Quelle, wird jeder Push verweigert.
Die Installation darf nicht am Erinnern haengen. .git/hooks wird weder
versioniert noch mitgeklont — ein frischer Klon des Ablegers hat keinen Guard,
und das faellt niemandem auf. Ein Satz in der Doku ("nach dem Clone neu
installieren") ist dafuer keine Kontrolle, sondern eine Hoffnung. Richtig:
das Sync-Skript installiert den Guard bei jedem Lauf selbst und verifiziert
die Kopie per shasum; schlaegt das fehl, bricht der Sync ab. Der Sync ist der
einzige Weg, auf dem ein Snapshot entsteht, also kann keiner ohne aktuellen
Guard existieren.
Dabei die Reihenfolge beachten: die Installation gehoert vor die
Inhalts-Gates. Steht sie dahinter, bleibt ein frischer Klon ausgerechnet dann
ungeschuetzt, wenn der Sync an einem Fund abbricht — also im Fehlerfall.
Realfall 23.07.2026: genau so zuerst gebaut und im Test aufgefallen. Den
Zeilenvergleich (Aufruf vor Gate) als Testfall festhalten; beim naechsten
Umbau geht die Reihenfolge sonst leise verloren.
PRIV=<privat>; PUB=<public>
cp "$PRIV/tools/public_push_guard.sh" "$PUB/.git/hooks/pre-push"
cp "$PRIV/tools/leak_markers.sh" "$PUB/.git/hooks/leak_markers.sh"
printf '%s\n' "$PRIV/tools/leak_markers.sh" > "$PUB/.git/hooks/leak_markers.source"
chmod +x "$PUB/.git/hooks/pre-push"
shasum -a 256 "$PUB/.git/hooks/pre-push" "$PRIV/tools/public_push_guard.sh" # muss gleich sein
Der Guard muss:
- jeden Commit im Push-Bereich scannen (
git rev-list "$local_oid" --not --remotes),
nicht nur die Spitze — ein Marker, der in Commit A auftaucht und in B
verschwindet, landet trotzdem im Objectstore des Remotes;
- Commit-Messages mitpruefen;
- Dateinamen gegen eine Verbotsliste pruefen;
- Binaerdateien/Bilder gesondert melden.
Vor jedem Push zusaetzlich, wenn es um eine Erstveroeffentlichung geht:
git log --format='%an %ae|%cn %ce' --all | sort -u # Klarnamen/Mails?
git log --diff-filter=D --name-only --all # geloescht != weg
git rev-list --objects --all | awk '{print $2}' | grep -iE '<verbotsmuster>'
git fsck --unreachable --dangling; git reflog --all # Amend-Reste?
Muster fuer den Inhalts-Scan: patterns.md im selben Ordner.
Das Push-Gate ist delta-basiert — Drift akkumuliert daran vorbei. Es prueft
den aktuellen Batch, nie den Bestand. Was ein frueherer Sync durchgelassen hat,
faellt keinem spaeteren mehr auf: es steht im Baum, aber in keinem Delta.
Realfall: drei deutsche Runtime-Strings aus alten Syncs in einem Ableger, der
English-only sein soll — von jedem Push-Gate seither uebersehen, weil niemand
sie mehr anfasste. Deshalb zusaetzlich zum Push-Gate
in groesseren Abstaenden einen Ganzbaum-Scan fahren (alle Marker gegen
git ls-tree -r HEAD, nicht gegen den Diff) und das Ergebnis mit Datum in der
Instanzdatei festhalten. Der Ganzbaum-Scan findet die Klasse, fuer die das
Push-Gate strukturell blind ist.
Regeln aus dem Audit vom 21.07.2026
- Test-Fixtures nie aus der Produktivdatei speisen. Der teuerste Fund des
Audits war kein Secret, sondern Zahlen: zwei echte Depotpositionen
(Stueckzahl, Stop-, Hochwert) standen bis auf die Nachkommastelle identisch
in zwei public Testdateien. Kein Marker, kein Pfadmuster und kein
Secret-Scanner schlaegt darauf an — nur ein wertbasierter Abgleich
gegen die private Datenquelle (
patterns.md §4). Fixtures kommen aus
*.example.* mit erfundenen Werten; muss eine Zahl echt sein, gehoert der
Test ins private Repo.
- Guard-Regexe brechen an der Schreibweise. Derselbe Guard hatte eine
Positionsgroessen-Pruefung — sie suchte
"shares": <bruch> (JSON,
Doppelquote) und uebersah 'shares': 24 (Python, Singlequote, ganzzahlig).
Formatbasierte Pruefungen immer gegen die echte Zielschreibweise testen.
Als Muster nachnutzbar: Quote-
Klasse ["'] statt fester Doppelquote, Feldklasse (shares|stk|stueck), und
der Wert-Check deckt jetzt jedes numerische Feld der Depotdatei ab statt
der vier, die der letzte Leak zufaellig benutzte. Zwei Lehren daraus:
- Ganzzahlen sind Depotdaten, nur nicht ueberall. Ein Wert-Check, der
Ganzzahlen ausschliesst (sonst ruft er Wolf), ist blind fuer jede
ganzzahlige Stueckzahl — und genau eine solche war der Fund. Loesung: das
PAAR suchen, nicht die Zahl.
'shares': 24 ist Depotdatum, ein loses 24
nicht. Damit faellt die Wolf-Frage weg und die Klasse ist trotzdem zu.
- Der Riegel gehoert eine Ebene frueher — und ist nicht optional. Der
Guard blockt den Push, also erst nachdem der Wert schon abgeschrieben ist.
Jedes Paar braucht deshalb im PRIVATEN Repo (a) einen billigen pytest, der
die Testsuite gegen
tools/leak_markers.sh grept, und (b) einen
wertbasierten Abgleich der Fixtures gegen die echte Datenquelle — inklusive
namentlicher Ausnahmeliste fuer Tests, die gegen einen Broker-Beleg
validieren und deshalb nie portiert werden duerfen. Zweiter Beleg: fehlt
dieser private Test, kommt dieselbe Fixture-Datei Tage spaeter mit denselben
echten IDs zurueck — der Push-Guard faengt die Wiederkehr nicht, er blockt
sie nur jedes Mal erneut.
- Overrides durch dasselbe Gate. Eine echte Geraete-UUID stand in einem
gescrubbten Public-README-Override. Weil der Override die Quelle ist,
re-publiziert ihn jeder kuenftige Sync. Ein Fix im public Repo allein
haelt nicht — immer die Override-Datei korrigieren.
- Gates nie an exakte Literale binden. Ein GPS-Gate, das auf die
sechsstellige Schreibweise der echten Koordinaten gebunden war, liess
dieselben Werte in gerundeter Form durch. Musterklassen matchen
(Koordinatenpaare, Ortsnamen), nicht Einzelwerte.
- Keine
--include-Endungslisten im Gate. Ein Gate ueber
py|yaml|md|ts|sh uebersieht json|csv|svg|html. Alle Textdateien
scannen, nur Binaeres ueberspringen.
- Marker koennen nur blocken, was sie kennen. Eine Pseudonym-Liste faengt
keinen unbekannten Klarnamen. Zusaetzlich generisch pruefen
(Vorname-Nachname-Paare,
@-Handles) und die Liste gegen die realen
Datenquellen gegenpruefen.
- Namensbasierte
.gitignore-Muster sind kein Schutz. Ein neuer Dateiname
rutscht durch (Realfall: Klartext-API-Key in einem neuen Skript). Deshalb
Allowlist.
- Der Sync scrubbt den Baum, nicht die History. Wer nachtraeglich
scrubbt, muss die History mitdenken — sonst ist der Fund nur unsichtbar,
nicht weg.
Weitere Regeln aus einem Sync eines hardware-nahen Ablegers
- Ein hartkodierter Echtwert ist selten nur ein Leak — das Gate ist zugleich
eine Code-Qualitaets-Sonde. Ein realer Ortsname stand als Default-Wert in
einem Job-Modul; dahinter ein latenter Bug: das genutzte Attribut sitzt auf
einer anderen Konfig-Klasse als der, die abgefragt wurde, also liefert der
getattr(obj, "feld", None)-Zugriff immer None und der hartkodierte
Fallback greift immer (im Fund folgenlos, weil die betroffenen Objekte vorher
uebersprungen wurden — ein zweiter Fall bekaeme still den falschen Wert).
Hartkodierte Echtwerte markieren oft einen fehlenden Konfig-Zugriff. Den Leak
verhaltenserhaltend fixen; die dahinterliegende Verhaltensaenderung an einem
Live-System ist eine getrennte Owner-Entscheidung, nicht Teil des Syncs.
- Begruendungs-Kommentare sind eine eigene Leak-Klasse (Muster:
patterns.md §5b). Docstrings, die warum erklaeren, zitieren den realen
Fall ("Objekt X (<echte-id>) fiel 77 -> 30"). Kein Secret- oder
Fixture-Muster trifft. Drei von fuenf Fundstellen dieses Syncs waren von
dieser Art.
Die andere Richtung: die oeffentliche Doku
Alle Gates oben pruefen, ob zu VIEL rausgeht. Die zweite Fehlerklasse ist, dass
zu WENIG rausgeht: der Ableger ist leak-sauber und trotzdem unbrauchbar, weil
die Doku den Stand nicht traegt. Kein Gate meldet das je — es ist immer
Handarbeit beim Sync.
Der Leser ist ein Fremder. Er hat deine Hardware nicht, deine Datendateien
nicht, dein Betriebssystem vielleicht nicht — und er kann nicht nachfragen. Die
private README darf Kontext voraussetzen, die oeffentliche nie.
- Der Quickstart muss auf einer fremden Maschine laufen. Als woertliche
Anweisung lesen, nicht als Erinnerungsstuetze. Realfall 27.07.2026: ein
Quickstart endete auf
open <datei> — macOS-only, bei ausdruecklich
internationaler Zielgruppe. Pruefpunkte: plattformspezifische Befehle
(open, pbcopy, Pfadtrenner), private Datendateien, die public gar nicht
existieren, vorausgesetzte Env-Variablen ohne .env.example-Eintrag.
- Entkoppelte Features muessen benannt sein. Ein Feature, das public per
find_spec gar nicht erst registriert wird (§A4), ist fuer den Leser
ununterscheidbar von einem Defekt. Ein Satz, was fehlt und warum
("laeuft gegen private Hardware"), verwandelt einen scheinbaren Bug in eine
nachvollziehbare Grenze.
- Nutzerbemerkbare Aenderung → README-Abschnitt, nicht nur CHANGELOG-Zeile.
Neue Config-Keys, neue Parameter, geaendertes Verhalten. Mechanisierbar:
neue Keys gegen README und
*.example.* diffen, bei Fehlbetrag warnen.
Realfall 27.07.2026 zweimal am selben Tag — einmal fehlte die Dosis-Semantik
in README und Beispiel-Config komplett, einmal wurde sie korrekt als eigener
Abschnitt ergaenzt.
- Ehrlichkeit ist Teil der Qualitaet. Was das Verfahren NICHT kann,
Limitationen, negative Ergebnisse, Datenherkunft und Lizenzen. Ein Backtest,
der auch die Verlustfaelle zeigt, ist glaubwuerdig; einer ohne sie ist
Werbung.
- Einen guten Text nicht umschreiben. Ist die README stark, ist die
Aenderung das Risiko, nicht der Stillstand. Dann nur den belegten Fehler
fixen und das im Bericht sagen — "ich habe sie gelesen und nichts gefunden"
ist ein vollwertiges Ergebnis.
Reihenfolge beim Sync: lesen → Delta bestimmen → gezielt aendern, nie
"README neu schreiben, weil Sync". Und die Aenderung gehoert in den
Override unter tools/public/, nie in den Ableger — sonst ueberschreibt
sie der naechste Sync.
Wenn doch etwas publiziert wurde
Reihenfolge, nicht verhandelbar: 1. rotieren, 2. dann erst rewriten.
Ein Secret gilt ab dem Push als kompromittiert — Clones, Forks und Crawler
sind nicht einholbar. Rewrite (filter-repo + force-push) ist Kosmetik fuer
den Objectstore, kein Ersatz fuer Rotation, und immer Einzelfreigabe des
Owners. Vorher gh repo view --json forkCount,stargazerCount — bei 0 Forks
ist ein Rewrite noch weitgehend wirksam.
Ein force-push loescht nichts — gemessen, nicht vermutet
Der ueberschriebene Commit bleibt bei GitHub liegen und wird per SHA weiter
ausgeliefert. Realfall 23.07.2026: nach commit --amend --reset-author +
push --force-with-lease lieferte
gh api repos/<owner>/<repo>/commits/<alter-sha> --jq '.commit.author.email'
die alte Klarnamen-Mail unveraendert zurueck — Author und Committer. Erst
nach Loeschen und Neuanlegen des Repos antwortet dieselbe Abfrage mit
HTTP 422 No commit found.
Erst die Gabelung pruefen: benigner oder gefaehrlicher Waise? Nicht jeder
ueberschriebene Commit ist eine Leak-Sorge — ein Message-only-Amend (etwa das
Nachruesten des Source:-Trailers) hinterlaesst einen Waisen, dessen Tree
byte-identisch und dessen Identitaet schon sauber ist. Bevor jemand wegen eines
abrufbaren alten SHA an Neuanlage denkt, die zwei Vergleiche laufen lassen:
git rev-parse "<alt>^{tree}" "<neu>^{tree}" # gleicher Tree?
git log -1 --format='%an %ae|%cn %ce' <alt> # schon Noreply?
Gleicher Tree und saubere Identitaet = kein Handlungsbedarf, nur benennen.
Erst wenn einer der beiden abweicht, gilt der Rest dieser Sektion. Ohne diese
Gabelung loest jeder Trailer-Fix einen Fehlalarm aus — und Fehlalarme sind der
Weg zur unnoetigen Repo-Neuanlage.
Konsequenzen fuer die Praxis:
- Ein Amend/Rewrite bereinigt den sichtbaren Stand, nicht die Historie des
Servers. Wer "X ist jetzt weg" behauptet, muss die Abfrage oben gelaufen
sein lassen. Das ist der einzige Beleg;
git log im Klon zeigt sie nie.
- Muss etwas wirklich weg, ist die Neuanlage das einzige Mittel. Preis sind
Stars, Forks, Issues und PR-History — bei einem frischen Showcase-Repo also
oft null. Vorher
description, topics, has_issues/has_projects sichern
(gh api repos/<o>/<r> --jq '{description,topics,has_issues,has_projects}');
LICENSE, CI-Workflows und Issue-Templates liegen im Tree und kommen mit dem
Push von selbst zurueck.
- Das Loeschen gehoert dem Owner, nicht dem Agenten. Es braucht ausserdem
den
delete_repo-Scope, den ein normaler gh-Token nicht hat
(gh auth refresh -h github.com -s delete_repo, oder Weboberflaeche →
Settings → Danger Zone). Den Scope hinterher wieder abraeumen.
- Reihenfolge: loeschen → privat neu anlegen → pushen → verifizieren →
erst dann auf public schalten. Nie in ein bereits oeffentliches Repo hinein
sanieren, wenn die Neuanlage ohnehin ansteht.
Instanzdaten — nicht hier, sondern am Repo
Dieser Skill ist agnostisch. Welches Paar existiert, welche History-Strategie es
faehrt, ob der Guard installiert ist und was offen steht, steht beim Repo:
- Ableger mit privatem Elternteil →
tools/public-mirror.md im privaten
Repo, neben sync-public.sh und leak_markers.sh.
- Repo ohne privaten Elternteil (Sichtbarkeits-Flip) →
.private/public-mirror.md
plus .gitignore-Eintrag:
git check-ignore -v .private/public-mirror.md
Verlass dich nicht auf den Default, pruefe den Ausschluss am Objekt. Regel 2
oben fordert eine Allowlist — ob das Sync-Skript sie tatsaechlich implementiert,
ist eine andere Frage. Realfall 23.07.2026: zwei Ableger fuhren einen
Default-Arm *) return 1 ;;, also public; in einem davon gingen fuenf neu
angelegte Dateien ohne Freigabe raus. Ein dritter hat gar kein Sync-Skript
(manueller Port) — dort schuetzt nur Handarbeit. Deshalb nach dem Anlegen der
Instanzdatei immer einmal:
tools/sync-public.sh --dry-run # oder Sync in einen Wegwerf-Zielordner
grep -rl "Instanzdaten" <ziel> # muss leer sein
Lies diese Datei, bevor du an einem Paar arbeitest. Ohne sie kennst du die
History-Strategie nicht — und ein Sync mit der falschen Strategie ist genau der
Fehler, den der Entscheidungsbaum oben verhindern soll.
Mindestinhalt: Repo-Paar mit aktueller Sichtbarkeit, History-Strategie,
Guard-Zustand, Besonderheiten (nicht rotierbare Datenklassen, Personendaten
Dritter), offene Punkte mit Datum, Stand-Datum, projektspezifische Markerwerte.
Warum getrennt: Instanzdaten veralten in dem Mass, in dem sie von ihrem
Gegenstand entfernt liegen. Eine Tabelle in ~/.claude/skills/ wird gepflegt,
wenn jemand daran denkt; eine Datei im Repo wird gepflegt, wenn das Repo sich
aendert. Realfall 23.07.2026: die fruehere Tabelle an dieser Stelle fuehrte
einen Ableger als publiziert, der laengst auf privat stand, und meldete einen
fehlenden Guard, den es gab — beides wurde geglaubt und war falsch. Und
27.07.2026: die prominenteste Warnung derselben Instanzdatei behauptete vier
Tage lang Blocklist-Default, obwohl das Skript laengst auf Allowlist
(*) return 0) stand. Regel darum verallgemeinert: jede Behauptung der
Instanzdatei am Objekt pruefen, mit dem Befehl daneben — nicht nur die
History-Strategie, auch Guard-Zustand, Default-Arm und Sichtbarkeit.
Fehlt dem Ableger der Source:-Trailer, ist sein Drift nur ueber Datum
schaetzbar — beim naechsten Sync nachruesten.
1---2name: public-mirror3description: Baut und pflegt oeffentliche Ableger privater Arbeits-Repos — Neuanlage, Sync und das Pre-Push-Leak-Gate. Verwende diesen Skill bei "mach ein public Repo aus ...", "sync das public Repo", "pruefe vor dem Push", "public Ableger aktualisieren", "kann das public werden", "Leak-Check vor Veroeffentlichung", "oeffentlichen Snapshot bauen". Auch wenn ein bestehendes Paar privat/public auseinanderzulaufen droht oder ein Audit der public History gefragt ist.4---56# Public-Mirror78Ein privates Arbeits-Repo bleibt privat. Der oeffentliche Ableger ist ein9**eigenes GitHub-Repo mit eigener Wurzel-History** — nie ein Filter-Zweig des10privaten.1112## Nicht verhandelbar13141. **Frische History.** Der Ableger startet mit eigenem Wurzel-Commit. Kein15 `filter-repo`-Abkoemmling des privaten Repos, nie `push --mirror`, nie16 `push refs/*`.172. **Allowlist, nicht Blocklist.** Default ist *privat*. Eine neu angelegte18 Datei landet **draussen**, bis jemand sie ausdruecklich freigibt.193. **Zwei Gates, eine Marker-Quelle.** Inhalts-Gate vor dem Commit +20 fail-closed `pre-push`-Hook. Beide sourcen dieselbe Marker-Datei.214. **Quelle ist `git ls-files`.** Nie den Worktree kopieren — sonst wandern22 untracked Dateien mit.235. **Read-only-Regel bei Audits.** Auf einem bereits publizierten Repo nie24 ungefragt `filter-repo`, force-push oder `gc`. Rewrite ist eine25 Einzelfreigabe pro Repo.2627## Sonderfall: Repo hat gar kein privates Elternteil2829Nicht jede Veroeffentlichung ist ein Ableger. Ein Repo, das von Anfang an30allein stand und nur auf `private` steht, wird durch einen **Sichtbarkeits-31Flip** public — kein Sync-Skript, keine Allowlist, kein zweites Repo. Die32Mirror-Maschinerie waere hier Ballast; es gilt stattdessen eine33Veroeffentlichungs-Checkliste:34351. **Alle Leak-Klassen aus `patterns.md` gegen alles scannen, was der36 *Server* kennt** — nicht gegen den Worktree: `main`, alle Tags, **und37 `refs/pull/*/head`**. Lokale, nie gepushte Branches werden nicht public.382. **LICENSE.** Ohne Lizenz heisst public "alle Rechte vorbehalten" — das39 Repo ist sichtbar, aber fuer Dritte unbenutzbar. Meist der eigentliche40 Zweck der Veroeffentlichung und trotzdem der haeufigste Fehlbetrag.413. **Steuerdateien** (`STATE.md`/`TASK.md`) untracken **und** aus der42 History entfernen. Ein blosses `git rm` laesst sie in allen Alt-Commits43 lesbar. Danach `.gitignore` + Verweise in verbleibender Doku aufloesen,44 sonst zeigt sie auf Dateien, die es im Repo nicht gibt.454. **Rewrite VOR dem Flip, nie danach** — solange privat, ist jeder Fehler46 noch folgenlos korrigierbar. Reihenfolge: Inhalt committen → Rewrite →47 force-push → Remote verifizieren → **dann** erst umschalten.485. Restrisiko benennen: nach dem force-push bleiben die alten Commits49 serverseitig unerreichbar liegen und sind per SHA noch eine Weile50 abrufbar. Airtight waere nur ein frisches Repo — Preis: Stars, Issues51 und PR-History. Bewusste Abwaegung des Owners, keine stille Entscheidung.5253## History-Strategie — Entscheidungsbaum5455```56Wird der Ableger aus dem privaten Repo REGENERIERT?57├─ ja → Squash-Snapshot: genau 1 Commit, Folge-Syncs per58│ `git commit --amend --reset-author`59│ + `git reflog expire --expire=now --all && git gc --prune=now`60│ Vorteil: ein spaeter enger gezogener Filter wirkt RUECKWIRKEND.61│ Preis: jeder Folge-Sync ist ein force-push.62│ **`--reset-author` ist nicht optional.** `--amend` allein tauscht nur63│ den COMMITTER und laesst den Author stehen — eine spaeter umgestellte64│ `user.email` (z. B. auf `<id>+<user>@users.noreply.github.com`) wirkt65│ deshalb nie, und die alte Klarnamen-Mail bleibt in jedem Folge-Sync66│ stehen. Nach dem Sync pruefen, nicht annehmen:67│ `git log --format='%an %ae|%cn %ce' --all | sort -u`68│ **CHANGELOG ist hier PFLICHT, nicht Kuer.** Ein Snapshot hat genau69│ einen Commit — die History traegt fuer einen fremden Leser null70│ Information darueber, was sich je geaendert hat. Der CHANGELOG ist71│ das einzige Artefakt, das Entwicklung ueberhaupt sichtbar macht.72│ Gemessen 27.07.2026: drei Ableger, alle 1 Commit, zwei ohne CHANGELOG.73└─ nein → Der Ableger wird eigenstaendig weiterentwickelt (CI, Issues, PRs).74 Organische History ist dann richtig — aber sie ist UNWIDERRUFLICH:75 alles muss beim ersten Push stimmen. Pflicht: Guard + CHANGELOG.7677NIE: akkumulierende Snapshot-History (jeder Sync ein Commit obendrauf, Inhalt78komplett neu erzeugt). Das ist die schlechteste Kombination — der Sync denkt79in "aktueller Stand", die History haelt jeden frueheren fest. Realfall: ein80Wohnort stand 151x in zwei Snapshots; der Scrub kam einen Tag spaeter und81konnte nichts mehr retten.82```8384## A) Neuen Ableger anlegen85861. **Inventur.** `git -C <priv> ls-files | wc -l`. Jede Datei genau einer87 Klasse zuordnen: *public*, *privat*, *gescrubbter Override*.882. **`tools/sync-public.sh` anlegen** mit `ist_privat()` als `case` —89 **Default-Arm `*) return 0 ;;`** (= privat). Public wird nur, was eine90 explizite Zeile freigibt. Praefix-Muster (`analysis/site-pool*`), nicht91 exakte Pfade: Varianten entstehen spaeter.923. **Overrides** nach `tools/public/` (README, LICENSE, `.env.example`,93 Beispiel-Configs). Achtung: Overrides sind selbst eine Leak-Quelle —94 sie muessen durch dasselbe Gate wie alles andere (siehe C).954. **Entkopplung statt Divergenz.** Fehlende private Module ueber96 `importlib.util.find_spec` erkennen und Features gar nicht erst97 registrieren — besser als kaputte Knoepfe oder ein zweiter Code-Zweig.985. **`tools/leak_markers.sh`** als einzige Marker-Quelle anlegen99 (Muster: `patterns.md` nebenan).1006. Sync laufen lassen, Gate muss gruen sein, Tests im Snapshot gruen.1017. **Repo zuerst privat anlegen**, pushen, Guard verifizieren, Inhalt102 durchsehen — **dann** auf public schalten.103104## B) Bestehenden Ableger syncen105106```bash107cd <privat> && git status --porcelain # sauber?108tools/sync-public.sh # Gate bricht VOR dem Commit ab109cd <public> && git push # Guard laeuft als pre-push110```111112- **Erst `git add`, dann syncen.** Quelle ist `git ls-files` — eine neue,113 noch nicht geaddete Datei ist fuer den Sync unsichtbar und fehlt im114 Snapshot, ohne Warnung.115- **`Source: <sha>` in die Commit-Message.** Ohne diesen Trailer ist der116 Drift nicht bestimmbar. Damit:117 `git -C <priv> rev-list --count <sha>..HEAD`118 **Ein privates `commit --amend` am Quell-Commit verwaist den Trailer.** Der119 `Source:`-SHA im Ableger zeigt danach auf einen Commit, den es nicht mehr gibt120 — und der naechste Sync repariert das NICHT, weil der Inhalt unveraendert ist121 ("nichts geaendert, kein Commit"). Die Drift-Rechnung bricht dann still. Vor122 jeder Drift-Aussage den Trailer-SHA auf Existenz pruefen, sonst nachziehen:123 `git -C <priv> cat-file -e "<trailer-sha>^{commit}" || echo "Trailer verwaist"`.124 Realfall: ein Message-Fix per Amend, danach zeigte der Trailer ins Leere.125 **Der Trailer misst Commit-Distanz, nicht Content-Drift.** Schreitet das126 private Repo durch public-irrelevante Commits fort (privates `tools/`, private127 Seiten), meldet `rev-list --count <sha>..HEAD` eine Drift, die inhaltlich128 keine ist. Die verlaessliche Currency-Probe ist der **No-op-Sync selbst**:129 laeuft er durch und meldet "nichts geaendert", ist der Ableger nachweislich130 aktuell — billiger und ehrlicher als jede Trailer-Distanz. Den Trailer fuer131 Nachvollziehbarkeit fuehren, die Aktualitaetsfrage mit dem Sync beantworten.132- **Der Sync ist tree-gated — Message- und Identitaets-Aenderungen traegt er133 nicht.** Er committet nur, wenn `git diff --cached` nicht leer ist. Alles, was134 ausschliesslich in der Commit-*Message* oder in Author/Committer lebt — ein135 nachgeruesteter `Source:`-Trailer, eine korrigierte Message, eine per136 `git config` umgestellte Mail — wird von einem Re-Sync **nie** aufgegriffen,137 solange der Tree gleich bleibt. Solche Aenderungen brauchen ein manuelles138 `git commit --amend --reset-author` im Ableger plus force-push. Realfall: ein139 nachgeruesteter Trailer, Sync meldete "nichts geaendert" und haette ihn nie140 gelandet.141- **Lokal != publiziert.** Vor jeder Aussage ueber "was ist oeffentlich":142 `git ls-remote <url> main` gegen `git rev-parse main`. Ein ungepushter143 Commit ist noch abwendbar, ein gepushter nicht.144- **Kein `--no-verify`.** Der Schalter kippt ALLE Pruefungen, nicht die eine,145 die meldet — und wirkt am staerksten beim ersten Push. Ein unerklaerlicher146 Guard-Fehler ist ein Grund abzubrechen, nicht zu umgehen.147- **Snapshot-Tests im Snapshot laufen lassen, nicht aus `tools/public/`.** Tests,148 die Konfig-/Fixture-Pfade relativ zu `__file__` aufloesen, melden aus dem149 Quellbaum Fehlschlaege, die wie echte Defekte aussehen — reine Diagnose-Falle.150 Realfall 27.07.2026: 22 Pfad-Artefakt-Fehlschlaege, alle folgenlos. Erst151 syncen, dann im Zielbaum testen.152- **Erst die History-Strategie feststellen, dann syncen.** `git log --oneline`153 im Ableger UND `git ls-remote <url>`: genau 1 Commit auf beiden Seiten heisst154 Squash-Snapshot, und dann ist der Sync `git commit --amend --reset-author`155 + force-push, nicht "Commit obendrauf". Wer das verwechselt, baut aus einem156 Squash-Repo genau die akkumulierende Snapshot-History, die der157 Entscheidungsbaum verbietet. Die Instanz-Tabelle unten ist eine KOPIE dieser158 Tatsache und kann veraltet sein (Realfall 23.07.2026: sie behauptete159 "organische History" fuer ein Squash-Repo) — im Zweifel gilt das Repo.160161## B2) Portieren statt Diffen — wenn der Ableger divergiert ist162163Ein Ableger, der laenger lebt, ist irgendwann **kein Spiegel mehr**: uebersetzte164Bezeichner, eigene Weiterentwicklung, Features die public frueher ankamen als165privat. Dann ist `git diff` zwischen den Repos wertlos und ein Diff-Apply166gefaehrlich — die private Aenderung muss auf die public Struktur **abgebildet**167werden. Reihenfolge, die sich bewaehrt hat:1681691. **Erst den public Ist-Stand feststellen, nicht den privaten Diff anwenden.**170 Was hat der Ableger von der Aenderung schon? Wo ist er eigenstaendig171 weitergegangen? Realfall: eine gemeinsame Hilfsschicht war public bereits da,172 aber an einer Stelle anders implementiert — ein Diff-Apply haette einen Test173 gebrochen, den es privat gar nicht gibt.1742. **Aenderung als Spezifikation lesen, nicht als Patch** — welches Verhalten,175 welche Felder, welche Einfuegestelle. Das Diff-Lesen laesst sich delegieren;176 die leak-sensible Anwendung nicht.1773. **Einfuegestelle im Ziel verifizieren, nicht raten** (Nachbarzeilen lesen),178 dann Feature fuer Feature portieren, jeweils mit gespiegeltem Test in der179 Zielsprache des Ablegers.1804. **Vorbestehende Defekte im Ziel sind nicht Teil des Ports.** Faellt beim181 Portieren ein Bug auf, der schon vorher da war: melden, nicht im Sync-Commit182 mitfixen — sonst vermischt sich Port und Reparatur, und der Commit wird183 unpruefbar.184185**Ein Task-Ticket spannt oft Portierbares UND Nicht-Portierbares.** Die186Ausschlussliste ist datei-basiert und trifft deshalb die falsche Einheit: eine187Aenderung kann eine generische Haelfte haben, die raus darf, und eine188personenbezogene, die nicht darf (Realfall: derselbe Task lieferte einen189generischen Steuer-Guard *und* Konto-Varianten-Strategien). Die Portier-Frage190deshalb **pro Hunk stellen, nicht pro Ticket** — und im Sync-Bericht191ausdruecklich benennen, welche Haelfte bewusst drinnen blieb.192193## C) Pre-Push-Gate — Checkliste194195Fail-closed: fehlt die Marker-Quelle, wird **jeder** Push verweigert.196197**Die Installation darf nicht am Erinnern haengen.** `.git/hooks` wird weder198versioniert noch mitgeklont — ein frischer Klon des Ablegers hat keinen Guard,199und das faellt niemandem auf. Ein Satz in der Doku ("nach dem Clone neu200installieren") ist dafuer keine Kontrolle, sondern eine Hoffnung. Richtig:201**das Sync-Skript installiert den Guard bei jedem Lauf selbst** und verifiziert202die Kopie per `shasum`; schlaegt das fehl, bricht der Sync ab. Der Sync ist der203einzige Weg, auf dem ein Snapshot entsteht, also kann keiner ohne aktuellen204Guard existieren.205206Dabei die **Reihenfolge** beachten: die Installation gehoert **vor** die207Inhalts-Gates. Steht sie dahinter, bleibt ein frischer Klon ausgerechnet dann208ungeschuetzt, wenn der Sync an einem Fund abbricht — also im Fehlerfall.209Realfall 23.07.2026: genau so zuerst gebaut und im Test aufgefallen. Den210Zeilenvergleich (Aufruf vor Gate) als Testfall festhalten; beim naechsten211Umbau geht die Reihenfolge sonst leise verloren.212213```bash214PRIV=<privat>; PUB=<public>215cp "$PRIV/tools/public_push_guard.sh" "$PUB/.git/hooks/pre-push"216cp "$PRIV/tools/leak_markers.sh" "$PUB/.git/hooks/leak_markers.sh"217printf '%s\n' "$PRIV/tools/leak_markers.sh" > "$PUB/.git/hooks/leak_markers.source"218chmod +x "$PUB/.git/hooks/pre-push"219shasum -a 256 "$PUB/.git/hooks/pre-push" "$PRIV/tools/public_push_guard.sh" # muss gleich sein220```221222Der Guard muss:223- **jeden Commit im Push-Bereich** scannen (`git rev-list "$local_oid" --not --remotes`),224 nicht nur die Spitze — ein Marker, der in Commit A auftaucht und in B225 verschwindet, landet trotzdem im Objectstore des Remotes;226- Commit-**Messages** mitpruefen;227- Dateinamen gegen eine Verbotsliste pruefen;228- Binaerdateien/Bilder gesondert melden.229230Vor jedem Push zusaetzlich, wenn es um eine Erstveroeffentlichung geht:231```bash232git log --format='%an %ae|%cn %ce' --all | sort -u # Klarnamen/Mails?233git log --diff-filter=D --name-only --all # geloescht != weg234git rev-list --objects --all | awk '{print $2}' | grep -iE '<verbotsmuster>'235git fsck --unreachable --dangling; git reflog --all # Amend-Reste?236```237238Muster fuer den Inhalts-Scan: **`patterns.md`** im selben Ordner.239240**Das Push-Gate ist delta-basiert — Drift akkumuliert daran vorbei.** Es prueft241den aktuellen Batch, nie den Bestand. Was ein frueherer Sync durchgelassen hat,242faellt keinem spaeteren mehr auf: es steht im Baum, aber in keinem Delta.243Realfall: drei deutsche Runtime-Strings aus alten Syncs in einem Ableger, der244English-only sein soll — von jedem Push-Gate seither uebersehen, weil niemand245sie mehr anfasste. Deshalb **zusaetzlich zum Push-Gate**246in groesseren Abstaenden einen **Ganzbaum-Scan** fahren (alle Marker gegen247`git ls-tree -r HEAD`, nicht gegen den Diff) und das Ergebnis mit Datum in der248Instanzdatei festhalten. Der Ganzbaum-Scan findet die Klasse, fuer die das249Push-Gate strukturell blind ist.250251## Regeln aus dem Audit vom 21.07.2026252253- **Test-Fixtures nie aus der Produktivdatei speisen.** Der teuerste Fund des254 Audits war kein Secret, sondern *Zahlen*: zwei echte Depotpositionen255 (Stueckzahl, Stop-, Hochwert) standen bis auf die Nachkommastelle identisch256 in zwei public Testdateien. Kein Marker, kein Pfadmuster und kein257 Secret-Scanner schlaegt darauf an — nur ein **wertbasierter** Abgleich258 gegen die private Datenquelle (`patterns.md` §4). Fixtures kommen aus259 `*.example.*` mit erfundenen Werten; muss eine Zahl echt sein, gehoert der260 Test ins private Repo.261- **Guard-Regexe brechen an der Schreibweise.** Derselbe Guard hatte eine262 Positionsgroessen-Pruefung — sie suchte `"shares": <bruch>` (JSON,263 Doppelquote) und uebersah `'shares': 24` (Python, Singlequote, ganzzahlig).264 Formatbasierte Pruefungen immer gegen die echte Zielschreibweise testen.265 Als Muster nachnutzbar: Quote-266 Klasse `["']` statt fester Doppelquote, Feldklasse `(shares|stk|stueck)`, und267 der Wert-Check deckt jetzt **jedes** numerische Feld der Depotdatei ab statt268 der vier, die der letzte Leak zufaellig benutzte. Zwei Lehren daraus:269 - **Ganzzahlen sind Depotdaten, nur nicht ueberall.** Ein Wert-Check, der270 Ganzzahlen ausschliesst (sonst ruft er Wolf), ist blind fuer jede271 ganzzahlige Stueckzahl — und genau eine solche war der Fund. Loesung: das272 PAAR suchen, nicht die Zahl. `'shares': 24` ist Depotdatum, ein loses 24273 nicht. Damit faellt die Wolf-Frage weg und die Klasse ist trotzdem zu.274 - **Der Riegel gehoert eine Ebene frueher — und ist nicht optional.** Der275 Guard blockt den Push, also erst nachdem der Wert schon abgeschrieben ist.276 Jedes Paar braucht deshalb im PRIVATEN Repo (a) einen billigen pytest, der277 die Testsuite gegen `tools/leak_markers.sh` grept, und (b) einen278 wertbasierten Abgleich der Fixtures gegen die echte Datenquelle — inklusive279 namentlicher Ausnahmeliste fuer Tests, die gegen einen Broker-Beleg280 validieren und deshalb nie portiert werden duerfen. Zweiter Beleg: fehlt281 dieser private Test, kommt dieselbe Fixture-Datei Tage spaeter mit denselben282 echten IDs zurueck — der Push-Guard faengt die Wiederkehr nicht, er blockt283 sie nur jedes Mal erneut.284- **Overrides durch dasselbe Gate.** Eine echte Geraete-UUID stand in einem285 gescrubbten Public-README-Override. Weil der Override die *Quelle* ist,286 re-publiziert ihn jeder kuenftige Sync. Ein Fix im public Repo allein287 haelt nicht — immer die Override-Datei korrigieren.288- **Gates nie an exakte Literale binden.** Ein GPS-Gate, das auf die289 sechsstellige Schreibweise der echten Koordinaten gebunden war, liess290 dieselben Werte in gerundeter Form durch. Musterklassen matchen291 (Koordinatenpaare, Ortsnamen), nicht Einzelwerte.292- **Keine `--include`-Endungslisten im Gate.** Ein Gate ueber293 `py|yaml|md|ts|sh` uebersieht `json|csv|svg|html`. Alle Textdateien294 scannen, nur Binaeres ueberspringen.295- **Marker koennen nur blocken, was sie kennen.** Eine Pseudonym-Liste faengt296 keinen unbekannten Klarnamen. Zusaetzlich generisch pruefen297 (Vorname-Nachname-Paare, `@`-Handles) und die Liste gegen die realen298 Datenquellen gegenpruefen.299- **Namensbasierte `.gitignore`-Muster sind kein Schutz.** Ein neuer Dateiname300 rutscht durch (Realfall: Klartext-API-Key in einem neuen Skript). Deshalb301 Allowlist.302- **Der Sync scrubbt den Baum, nicht die History.** Wer nachtraeglich303 scrubbt, muss die History mitdenken — sonst ist der Fund nur unsichtbar,304 nicht weg.305306## Weitere Regeln aus einem Sync eines hardware-nahen Ablegers307308- **Ein hartkodierter Echtwert ist selten nur ein Leak — das Gate ist zugleich309 eine Code-Qualitaets-Sonde.** Ein realer Ortsname stand als Default-Wert in310 einem Job-Modul; dahinter ein latenter Bug: das genutzte Attribut sitzt auf311 einer anderen Konfig-Klasse als der, die abgefragt wurde, also liefert der312 `getattr(obj, "feld", None)`-Zugriff **immer** `None` und der hartkodierte313 Fallback greift immer (im Fund folgenlos, weil die betroffenen Objekte vorher314 uebersprungen wurden — ein zweiter Fall bekaeme still den falschen Wert).315 Hartkodierte Echtwerte markieren oft einen fehlenden Konfig-Zugriff. Den Leak316 verhaltenserhaltend fixen; die dahinterliegende Verhaltensaenderung an einem317 Live-System ist eine getrennte Owner-Entscheidung, nicht Teil des Syncs.318- **Begruendungs-Kommentare sind eine eigene Leak-Klasse** (Muster:319 `patterns.md` §5b). Docstrings, die *warum* erklaeren, zitieren den realen320 Fall ("Objekt X (`<echte-id>`) fiel 77 -> 30"). Kein Secret- oder321 Fixture-Muster trifft. Drei von fuenf Fundstellen dieses Syncs waren von322 dieser Art.323324## Die andere Richtung: die oeffentliche Doku325326Alle Gates oben pruefen, ob zu VIEL rausgeht. Die zweite Fehlerklasse ist, dass327zu WENIG rausgeht: der Ableger ist leak-sauber und trotzdem unbrauchbar, weil328die Doku den Stand nicht traegt. Kein Gate meldet das je — es ist immer329Handarbeit beim Sync.330331**Der Leser ist ein Fremder.** Er hat deine Hardware nicht, deine Datendateien332nicht, dein Betriebssystem vielleicht nicht — und er kann nicht nachfragen. Die333private README darf Kontext voraussetzen, die oeffentliche nie.3343351. **Der Quickstart muss auf einer fremden Maschine laufen.** Als woertliche336 Anweisung lesen, nicht als Erinnerungsstuetze. Realfall 27.07.2026: ein337 Quickstart endete auf `open <datei>` — macOS-only, bei ausdruecklich338 internationaler Zielgruppe. Pruefpunkte: plattformspezifische Befehle339 (`open`, `pbcopy`, Pfadtrenner), private Datendateien, die public gar nicht340 existieren, vorausgesetzte Env-Variablen ohne `.env.example`-Eintrag.3412. **Entkoppelte Features muessen benannt sein.** Ein Feature, das public per342 `find_spec` gar nicht erst registriert wird (§A4), ist fuer den Leser343 ununterscheidbar von einem Defekt. Ein Satz, was fehlt und warum344 ("laeuft gegen private Hardware"), verwandelt einen scheinbaren Bug in eine345 nachvollziehbare Grenze.3463. **Nutzerbemerkbare Aenderung → README-Abschnitt, nicht nur CHANGELOG-Zeile.**347 Neue Config-Keys, neue Parameter, geaendertes Verhalten. Mechanisierbar:348 neue Keys gegen README **und** `*.example.*` diffen, bei Fehlbetrag warnen.349 Realfall 27.07.2026 zweimal am selben Tag — einmal fehlte die Dosis-Semantik350 in README und Beispiel-Config komplett, einmal wurde sie korrekt als eigener351 Abschnitt ergaenzt.3524. **Ehrlichkeit ist Teil der Qualitaet.** Was das Verfahren NICHT kann,353 Limitationen, negative Ergebnisse, Datenherkunft und Lizenzen. Ein Backtest,354 der auch die Verlustfaelle zeigt, ist glaubwuerdig; einer ohne sie ist355 Werbung.3565. **Einen guten Text nicht umschreiben.** Ist die README stark, ist die357 Aenderung das Risiko, nicht der Stillstand. Dann nur den belegten Fehler358 fixen und das im Bericht sagen — "ich habe sie gelesen und nichts gefunden"359 ist ein vollwertiges Ergebnis.360361Reihenfolge beim Sync: **lesen → Delta bestimmen → gezielt aendern**, nie362"README neu schreiben, weil Sync". Und die Aenderung gehoert in den363**Override** unter `tools/public/`, nie in den Ableger — sonst ueberschreibt364sie der naechste Sync.365366## Wenn doch etwas publiziert wurde367368Reihenfolge, nicht verhandelbar: **1. rotieren, 2. dann erst rewriten.**369Ein Secret gilt ab dem Push als kompromittiert — Clones, Forks und Crawler370sind nicht einholbar. Rewrite (`filter-repo` + force-push) ist Kosmetik fuer371den Objectstore, kein Ersatz fuer Rotation, und **immer Einzelfreigabe des372Owners**. Vorher `gh repo view --json forkCount,stargazerCount` — bei 0 Forks373ist ein Rewrite noch weitgehend wirksam.374375### Ein force-push loescht nichts — gemessen, nicht vermutet376377Der ueberschriebene Commit bleibt bei GitHub liegen und wird **per SHA weiter378ausgeliefert**. Realfall 23.07.2026: nach `commit --amend --reset-author` +379`push --force-with-lease` lieferte380381```bash382gh api repos/<owner>/<repo>/commits/<alter-sha> --jq '.commit.author.email'383```384385die alte Klarnamen-Mail unveraendert zurueck — Author *und* Committer. Erst386nach Loeschen und Neuanlegen des Repos antwortet dieselbe Abfrage mit387HTTP 422 `No commit found`.388389**Erst die Gabelung pruefen: benigner oder gefaehrlicher Waise?** Nicht jeder390ueberschriebene Commit ist eine Leak-Sorge — ein Message-only-Amend (etwa das391Nachruesten des `Source:`-Trailers) hinterlaesst einen Waisen, dessen Tree392byte-identisch und dessen Identitaet schon sauber ist. Bevor jemand wegen eines393abrufbaren alten SHA an Neuanlage denkt, die zwei Vergleiche laufen lassen:394395```bash396git rev-parse "<alt>^{tree}" "<neu>^{tree}" # gleicher Tree?397git log -1 --format='%an %ae|%cn %ce' <alt> # schon Noreply?398```399400Gleicher Tree **und** saubere Identitaet = kein Handlungsbedarf, nur benennen.401Erst wenn einer der beiden abweicht, gilt der Rest dieser Sektion. Ohne diese402Gabelung loest jeder Trailer-Fix einen Fehlalarm aus — und Fehlalarme sind der403Weg zur unnoetigen Repo-Neuanlage.404405Konsequenzen fuer die Praxis:406407- **Ein Amend/Rewrite bereinigt den sichtbaren Stand, nicht die Historie des408 Servers.** Wer "X ist jetzt weg" behauptet, muss die Abfrage oben gelaufen409 sein lassen. Das ist der einzige Beleg; `git log` im Klon zeigt sie nie.410- **Muss etwas wirklich weg, ist die Neuanlage das einzige Mittel.** Preis sind411 Stars, Forks, Issues und PR-History — bei einem frischen Showcase-Repo also412 oft null. Vorher `description`, `topics`, `has_issues`/`has_projects` sichern413 (`gh api repos/<o>/<r> --jq '{description,topics,has_issues,has_projects}'`);414 LICENSE, CI-Workflows und Issue-Templates liegen im Tree und kommen mit dem415 Push von selbst zurueck.416- **Das Loeschen gehoert dem Owner**, nicht dem Agenten. Es braucht ausserdem417 den `delete_repo`-Scope, den ein normaler `gh`-Token nicht hat418 (`gh auth refresh -h github.com -s delete_repo`, oder Weboberflaeche →419 Settings → Danger Zone). Den Scope hinterher wieder abraeumen.420- **Reihenfolge:** loeschen → **privat** neu anlegen → pushen → verifizieren →421 erst dann auf public schalten. Nie in ein bereits oeffentliches Repo hinein422 sanieren, wenn die Neuanlage ohnehin ansteht.423424## Instanzdaten — nicht hier, sondern am Repo425426Dieser Skill ist agnostisch. Welches Paar existiert, welche History-Strategie es427faehrt, ob der Guard installiert ist und was offen steht, steht **beim Repo**:428429- Ableger mit privatem Elternteil → `tools/public-mirror.md` im **privaten**430 Repo, neben `sync-public.sh` und `leak_markers.sh`.431- Repo ohne privaten Elternteil (Sichtbarkeits-Flip) → `.private/public-mirror.md`432 **plus `.gitignore`-Eintrag**:433 `git check-ignore -v .private/public-mirror.md`434435**Verlass dich nicht auf den Default, pruefe den Ausschluss am Objekt.** Regel 2436oben fordert eine Allowlist — ob das Sync-Skript sie tatsaechlich implementiert,437ist eine andere Frage. Realfall 23.07.2026: zwei Ableger fuhren einen438Default-Arm `*) return 1 ;;`, also **public**; in einem davon gingen fuenf neu439angelegte Dateien ohne Freigabe raus. Ein dritter hat gar kein Sync-Skript440(manueller Port) — dort schuetzt nur Handarbeit. Deshalb nach dem Anlegen der441Instanzdatei immer einmal:442443```bash444tools/sync-public.sh --dry-run # oder Sync in einen Wegwerf-Zielordner445grep -rl "Instanzdaten" <ziel> # muss leer sein446```447448**Lies diese Datei, bevor du an einem Paar arbeitest.** Ohne sie kennst du die449History-Strategie nicht — und ein Sync mit der falschen Strategie ist genau der450Fehler, den der Entscheidungsbaum oben verhindern soll.451452Mindestinhalt: Repo-Paar mit **aktueller** Sichtbarkeit, History-Strategie,453Guard-Zustand, Besonderheiten (nicht rotierbare Datenklassen, Personendaten454Dritter), offene Punkte mit Datum, Stand-Datum, projektspezifische Markerwerte.455456Warum getrennt: Instanzdaten veralten in dem Mass, in dem sie von ihrem457Gegenstand entfernt liegen. Eine Tabelle in `~/.claude/skills/` wird gepflegt,458wenn jemand daran denkt; eine Datei im Repo wird gepflegt, wenn das Repo sich459aendert. Realfall 23.07.2026: die fruehere Tabelle an dieser Stelle fuehrte460einen Ableger als publiziert, der laengst auf privat stand, und meldete einen461fehlenden Guard, den es gab — beides wurde geglaubt und war falsch. Und46227.07.2026: die prominenteste Warnung derselben Instanzdatei behauptete vier463Tage lang Blocklist-Default, obwohl das Skript laengst auf Allowlist464(`*) return 0`) stand. Regel darum verallgemeinert: **jede** Behauptung der465Instanzdatei am Objekt pruefen, mit dem Befehl daneben — nicht nur die466History-Strategie, auch Guard-Zustand, Default-Arm und Sichtbarkeit.467468Fehlt dem Ableger der `Source:`-Trailer, ist sein Drift nur ueber Datum469schaetzbar — beim naechsten Sync nachruesten.