PullMD Teil 4: Vom Tool für mich zum OSS-Projekt
Der Vormittag des 25. April hatte ich damit verbracht, den Trafilatura-Sidecar zu integrieren und den Bug zu fixen, der wochenlang in einem Default-Parameter schlummerte — beides Thema von Teil 3. Nachmittags stand dann die Sache an, die ich seit Wochen vor mir herschob: Public-Release auf GitHub. Der Code war bereit. Die README noch nicht. Und der Codename war im Source-Tree noch nicht durch den realen Namen ersetzt — aus zwei Gründen, die seit Anfang März im Kopf herumlagen.
Der Codename passt nicht mehr (Grund 1)
Der Codename, unter dem das Tool entstanden war, kam aus dem allerersten Konzept-Chat: ich hatte Claude gebeten, „such dir was aus“ für die Subdomain, und Claude hatte einen Vorschlag geliefert, weil das Ding Reddit-Content „pullt“ und der Name kurz für ein App-Icon ist. Klares Wortspiel, funktional, sofort verwendbar. Ich habe ihn übernommen, weil ich zu dem Zeitpunkt nicht über Branding nachgedacht habe — der Service lief im Homelab unter mir allein, nicht unter einer Marke.
Inzwischen war das Tool etwas anderes. Drei Extraction-Pfade — Cloudflare-nativer Markdown, der Reddit-JSON-Pfad, und der Readability-plus-Trafilatura-Stack für alles andere. Quality-Score. MCP-Server-Schnittstelle. Generierbares Skill-Bundle. PWA-Frontend mit Web Share Target. Jeder dieser Pfade funktioniert für jede URL, nicht nur für Reddit.
Wenn jemand den Repo-Namen liest und dort eine Reddit-Referenz sieht, ist die erste Erwartung: Reddit-spezifisches Tool. Das stimmt nicht mehr. Ein Name, der falsche Erwartungen setzt, ist am Ende weniger ein Branding-Problem als eine Support-Frage, die noch keiner gestellt hat. Allein schon der Klarheit halber musste ein neutraler Name her.
Der Codename war juristisch grenzwertig (Grund 2)
Das Markenrecht-Problem hatte ich nicht erst zum Push-Zeitpunkt im Kopf. Wenige Tage nach dem Konzept-Chat — in einem separaten KI-Gespräch, an dessen Wortlaut ich mich nicht mehr genau erinnere — wurde mir klar, dass die phonetische Nähe zu einer großen Marke aus dieser Konsum-Kategorie konkret riskant war. Die Firma hinter der Marke ist dafür bekannt, auch gegen kleine Projekte vorzugehen, wenn ein Name phonetisch ähnlich klingt — das ist dokumentierte Praxis, nicht meine Interpretation. Die Wahrscheinlichkeit, dass das auf ein wachsendes Open-Source-Projekt unter ähnlichem Namen zukommt, war hoch genug, dass ich den Codenamen ab da nicht mehr als geplanten finalen Namen behandelt habe.
Schon am 8. März habe ich das Production-Deploy-Verzeichnis unter dem neuen Namen /home/user/pullmd/ angelegt — der Brand-Wechsel war damit faktisch entschieden. In Gesprächen mit Claude habe ich seit Mitte März über „PullMD“ geredet. Nur der Source-Code-Tree blieb pragmatisch unter dem Codenamen, weil’s intern erstmal egal war: ein einzelner User auf einem einzelnen Server, niemand sonst sieht die Repo-Struktur.
Vor dem Public-Push wurde aus „intern egal“ jetzt „muss aufgeräumt werden“. Die Wahl war einfach: 30 Minuten gründliche Source-Tree-Bereinigung, oder das Risiko unangenehmer Post von einer Markenrecht-Kanzlei. Die 30 Minuten schienen sinnvoller.
PullMD — neutral, beschreibend, niemandem auf den Füßen
Pull und MD. Der Service zieht Inhalte von URLs herunter und gibt Markdown zurück. Der Name beschreibt was das Tool tut, und nicht mehr. Kein Wortspiel, keine Pop-Kultur-Referenz, keine phonetische Nähe zu irgendeiner Marke in irgendeiner Kategorie.
„Sometimes the best name is the boring one.“ PullMD ist boring in genau der richtigen Weise: jemand, der den Namen liest, weiß sofort, was er erwarten kann. Das ist alles, was ein Tool-Name leisten muss.
Verzeichnis-Migration
Auf meinem Ubuntu Docker-Host war damit nichts zu tun: Production lief seit dem 8. März unter /home/user/pullmd/, der Pfad war seit der Markenrecht-Erkenntnis schon der „echte“ Name. Was offen war, war der Source-Code-Tree und alle internen Verweise dort: Repo-Name auf meinem Ubuntu-Entwicklungsserver, CLAUDE.md, Memory-Files, README-Snippets, Skill-Bundle-Config. Plus die Frage, was mit der Commit-History passiert — die war voll mit dem alten Codenamen.
Wir haben uns für den gründlichen Weg entschieden: Source-Repo umbenannt, Commit-History gerewritet, interne Notizen synchronisiert. Im Grunde nur die längst fällige Bereinigung — der reale Name war seit sieben Wochen unterwegs, der Source-Tree zog jetzt nach.
Notiz am Rand: Was Git nachträglich noch ändern kann
Ehrlich gesagt: Claude Code hat den Rename samt History-Rewrite einfach gemacht. Welche Befehle dafür gelaufen sind, wie filter-repo eigentlich funktioniert, was schiefgeht wenn man’s falsch macht — hat mich zu dem Zeitpunkt nicht interessiert. Es war Migrations-Plumbing, ich wollte Public gehen.
Erst beim Schreiben dieses Artikels habe ich nachgeschaut, was tatsächlich gelaufen ist. Drei Operationen:
- Author-Identitäten umschreiben —
git filter-repo --mailmap mailmap.txt. Einemailmap.txtmit Zeilen der FormNew Name <new@email> = Old Name <old@email>, dann tauscht filter-repo in jedem alten Commit den Author entsprechend. In meinem Fall: meine persönliche Mail-Adresse aus den Codenamen-Tagen wurde durch die OSS-Persona ersetzt, damit die Public-History konsistent unter der einen Identität läuft. - Text in alten Commits ersetzen —
git filter-repo --replace-text replacements.txt. Eine Datei mit Regeln der Formalter-string==>neuer-string, filter-repo geht durch jede Datei in jedem alten Commit. So verschwand der Codename auch aus Commit-Diffs, Code-Kommentaren und alten CLAUDE.md-Snapshots, die in der History noch herumlagen. - Anschließend Branches und Tags resetten — Nach filter-repo haben alle alten Commits neue SHAs, die History ist effektiv neu. Vor dem ersten Public-Push ist das harmlos: einmal sauber pushen, fertig. Bei einem bereits öffentlichen Repo bräuchte man
--force-with-lease, und alle existierenden Forks und Clones würden divergieren. Daher der Standardrat: solche History-Rewrites möglichst vor dem ersten Public-Push machen.
Das Tool selbst ist nicht im Standard-Git enthalten, sondern ein separates Python-Paket: pip install git-filter-repo. Offizieller Nachfolger des alten git filter-branch, das so viele Stolpersteine hatte, dass die Git-Maintainer selbst auf filter-repo verweisen.
Jetzt weiß ich’s. Damals: zugesehen, gewartet, gepusht. Wenn man Vertrauen in das Tool hat, ist das ein legitimer Modus — die Operation ist deterministisch, vor dem ersten Public-Push gibt’s keinen Schaden, den man nicht zurücknehmen kann. Wenn man kein Vertrauen hat, sollte man die Befehle vor dem Delegieren verstehen, nicht danach beim Bloggen.
Das kritischere Stück: das Session-Verzeichnis
Das kritischere Stück war das Claude-Code-Session-Verzeichnis. Die Sessions sind die Quelle, aus der diese Blogserie selbst geschrieben wird — ohne sie keine archäologische Rekonstruktion von Teil 2 oder 3. Claude Code legt JSONL-Transkripte in einem Pfad ab, der vom Working Directory abgeleitet wird — ~/.claude/projects/-home-user-projects-<verzeichnisname>/. Wenn ich nach der Migration versehentlich aus dem alten Verzeichnis heraus Claude Code starte, landen neue Sessions im alten Pfad und sind nicht mehr zusammen mit der neuen Session-Historie. Als kleiner Schutz vor dem Fall: _archive/sync-sessions.sh, ein Helper-Script, das die neue Session-Historie in den aktualisierten Pfad spiegelt.
Und dann war da noch das Tar-Backup: das gesamte alte Session-Verzeichnis gesichert als _archive/claude-sessions-<codename>-2026-04-25.tar.gz (2,3 MB). Dieses Archiv ist übrigens genau das, was diese Blogserie erst möglich macht. Die Sessions vom 20. März, 11. April und 25. April — auf denen Teil 2, 3 und dieser Artikel hier aufbauen — wären ohne dieses Backup nicht mehr zitierbar gewesen. Das tar.gz liegt in _archive/, wird nie ins Repo committed, aber ist die Quelle für alles, was hier als „aus der Session zitiert“ erscheint.
AGPL-3.0, nicht MIT
Bei Open-Source-Projekten läuft die Lizenzwahl oft auf eine kurze Abwägung hinaus: MIT wenn man maximale Verbreitung will, GPL wenn man ideologisch überzeugt ist, und irgendwas dazwischen wenn man nicht genau weiß was man will. Das ist keine besonders hilfreiche Kategorisierung.
Die eigentliche Frage für PullMD war: was passiert, wenn jemand das forkt, Modifikationen einbaut, und als Cloud-Service anbietet — ohne die Modifikationen zu teilen? Bei MIT ist die Antwort: nichts. Das ist die ganze Idee permissiver Lizenzen. Für ein CLI-Tool oder eine Library ist das die richtige Wahl. Breite Adoption, niedrige Reibung, fertig.
Für einen selbst-gehosteten Service ist die Frage eine andere. PullMD ist kein passiv genutztes Paket — es ist ein Service, der läuft und Requests beantwortet. Wenn jemand das als Cloud-Dienst betreibt und dabei Verbesserungen einbaut, die ich nie zu Gesicht bekomme, verliere ich den Anreiz zu verstehen, was die eigentlichen Probleme in meiner Codebasis sind. AGPL-3.0-or-later ist genau für diesen Fall: Wer den Service über Netzwerk anbietet und Modifikationen gemacht hat, muss diese Modifikationen unter AGPL veröffentlichen.
Das ist keine ideologische Aussage über Free Software. Es ist eine Frage der Anreizstruktur. CLI-Helper, Libraries, Toolchain-Pakete: MIT oder Apache-2.0 sind die richtigen Werkzeuge. Self-hosted Services, bei denen Netzwerknutzung der primäre Kanal ist: AGPL überlegen. Die permissiven Dependencies tief im Stack — Express, cheerio, better-sqlite3 — bleiben frei wählbar; Lizenz-Layering ist kein binäres Entweder-Oder. Mehr Nuance zu den Konsequenzen einzelner Lizenzen ohne Anwalt: choosealicense.com.
Self-Host-fähig machen
Bis zu diesem Punkt lief der Service nur auf meiner Infrastruktur, mit meinen Hostnamen hartkodiert an verschiedenen Stellen. Für einen Public-Release musste das anders werden.
Die wichtigste Änderung war PUBLIC_URL als konfigurierbare Umgebungsvariable. Der Default-Wert ist https://${HOST_DOMAIN} — wer HOST_DOMAIN für Traefik setzt, bekommt automatisch die korrekte URL. Diese Variable wird in der /help-Seite substituiert und in das /web-reader.zip eingebettet, ein generierbares Skill-Bundle für Claude Code. Ohne die Substitution würden Self-Hoster auf der Help-Seite meinen Hostnamen sehen, nicht ihren eigenen — eine schlechte Erfahrung und eine unnötige Abhängigkeit.
HOST_DOMAIN steuert das Traefik-Label in docker-compose.yml. Wer das auf einer eigenen Domain deployen will, setzt diese eine Variable, und Traefik routet und stellt Zertifikate aus.
Ein sauberes .env.example als Template — die echte .env mit OAuth-Credentials bleibt außerhalb des Repos. Die README wurde komplett für Public Consumption umgeschrieben: keine internen Notizen, kein Eval-Workspace, keine Briefing-Dokumente. .gitignore deckt alles ab, was nicht nach außen soll: CLAUDE.md, SESSION_LOG.md, pullmd-improvements-briefing.md, docs/, .claude/, _archive/.
Der Push
gh repo create AeternaLabsHQ/pullmd --public --source=. --remote=origin --push
Der lokale Initial-Public-Release-Commit war am 25. April um 18:12 fertig. Bis der Push tatsächlich lief, vergingen aber noch zwei Tage Polish — v1.0.0 und v1.0.1 wurden lokal getagt, aber nicht gepusht. v1.0.1 war ein Reddit-Comment-Cache-Bugfix, der erst auf meinem Ubuntu Docker-Host live musste, bevor der Repo öffentlich werden sollte. Am 27. April um 09:32 morgens lief gh repo create dann durch — v1.0.2 ist der erste Tag in der Public-History. In den folgenden Tagen kamen UI-Polish-Commits — Footer, Version-Templating, Help-Page-Layout. Das sind die normalen Letzte-Schliff-Sachen, die einem immer erst auffallen, wenn man das erste Mal mit echtem Abstand auf den eigenen Service schaut.
Ein Tool, das im März als Reddit-Spielzeug begann, war acht Wochen später offiziell Open Source — für jede URL, unter github.com/AeternaLabsHQ/pullmd.
Was ich jetzt eigentlich habe
Ein laufender HTTP-Service auf eigener Domain, den ich jeden Tag benutze — für Reddit-Posts, für Artikel, für Dokumentationsseiten, für alles, was ich als sauberes Markdown in einen Chat-Kontext bringen will.
Darunter: drei Extraction-Pfade mit Quality-Score, ein MCP-Server für Claude Code, ein generierbares Skill-Bundle, eine PWA für den Android-Share-Workflow. P3.1 läuft seit dem 25. April — eine SQLite-Tabelle mit Extraction-Log, ein /api/stats-Endpoint. Eine Woche Daten sammeln, dann entscheide ich ob P3.2 (Playwright-Microservice für JS-only-Sites) sich lohnt oder Feature-Creep wäre. Beide könnten nützlich sein. Ob sie nötig sind, zeigt der Stats-Endpoint.
Das Projekt ist öffentlich auf GitHub, AGPL-3.0, README für andere lesbar. Und es gibt diese Blogserie, die den Bogen von einer frustrierenden Strg+A-Aktion bis zum OSS-Release nachzeichnet — mit echten Sessions als Quelldokumenten, nicht als rekonstruierter Nacherzählung.
Was als Strg+A-Frust anfing, ist acht Wochen später ein Stück Software, das ich täglich nutze. Mit oder ohne Open Source — das war den Aufwand wert.
Teil 4 von 9 der PullMD Serie.
← PullMD Teil 3: Mehr als Reddit, mehr als Readability | PullMD Teil 5: Reddit-Tag und die ersten Issues →