PullMD Part 4: From My Tool to an OSS Project
I had spent the morning of April 25 integrating the Trafilatura sidecar and fixing the bug that had been sleeping in a default parameter for weeks — both the subject of Part 3. The afternoon was reserved for something I had been putting off for weeks: the public release on GitHub. The code was ready. The README wasn’t yet. And the codename hadn’t yet been swept out of the source tree and replaced with the real name — for two reasons that had been sitting in my head since early March.
The Codename Doesn’t Fit Anymore (Reason 1)
The codename the tool had grown up under came out of the very first concept chat: I had asked Claude to “pick something” for the subdomain, and Claude came back with a suggestion — because the thing pulls Reddit content and the name was short enough for an app icon. Clear wordplay, functional, immediately usable. I took it because at that point I wasn’t thinking about branding — the service ran in my homelab, just for me, not under a brand.
By now the tool was something different. Three extraction paths — Cloudflare-native markdown, the Reddit JSON path, and the Readability-plus-Trafilatura stack for everything else. Quality score. MCP server interface. Generatable skill bundle. PWA frontend with Web Share Target. Every one of those paths works for any URL, not just Reddit.
When someone reads the repo name and sees a Reddit reference, the first expectation is: Reddit-specific tool. That’s no longer accurate. A name that sets the wrong expectations isn’t a branding problem — it’s a support question that hasn’t been asked yet. From pure naming hygiene alone, something neutral was needed.
The Codename Was Legally Borderline (Reason 2)
The trademark issue wasn’t something I only noticed at push time. A few days after the concept chat — in a separate AI conversation whose exact wording I no longer remember — it became clear that the phonetic proximity to a major brand from this consumer category was a concrete risk. The company behind that brand is known for going after small projects when a name sounds phonetically similar — that’s documented practice, not my interpretation. The probability that something like that would land on a growing open-source project under a similar name was high enough that I stopped treating the codename as the planned final name from that point on.
On March 8 already, I created the production deploy directory under the new name /home/user/pullmd/ — the brand switch was effectively decided then. In conversations with Claude, I had been calling it “PullMD” since mid-March. Only the source-code tree stayed pragmatically under the codename, because internally it didn’t matter: a single user on a single server, nobody else seeing the repo structure.
Before the public push, “internally it didn’t matter” turned into “now it has to be cleaned up.” The choice was simple: 30 minutes of thorough source-tree cleanup, or the risk of an unpleasant letter from a trademark firm. The 30 minutes seemed more sensible.
PullMD — Neutral, Descriptive, Stepping on No One’s Toes
Pull and MD. The service pulls content from URLs and returns markdown. The name describes what the tool does, and nothing more. No wordplay, no pop-culture reference, no phonetic similarity to any brand in any category.
“Sometimes the best name is the boring one.” PullMD is boring in exactly the right way: someone who reads the name immediately knows what to expect. That’s all a tool name needs to do.
Directory Migration
On my Ubuntu Docker host there was nothing to do: production had been running under /home/user/pullmd/ since March 8, the path had been the “real” name ever since the trademark realization. What was open was the source-code tree and all internal references in it: repo name on my Ubuntu development server, CLAUDE.md, memory files, README snippets, skill bundle config. Plus the question of what to do with the commit history — that was full of the old codename.
We went the thorough route: source repo renamed, commit history rewritten, internal notes synchronized. Essentially just the long-overdue cleanup — the real name had been in use for seven weeks, the source tree was finally catching up.
A Note in the Margin: What Git Can Still Change After the Fact
Honestly: Claude Code just did the rename plus history rewrite. Which commands ran, how filter-repo actually works, what goes wrong if you do it incorrectly — none of that interested me at the time. It was migration plumbing; I wanted to go public.
Only while writing this article did I look up what actually happened. Three operations:
- Rewriting author identities —
git filter-repo --mailmap mailmap.txt. Amailmap.txtwith lines of the formNew Name <new@email> = Old Name <old@email>, then filter-repo walks every old commit and substitutes the author. In my case: my personal email address from the codename days was replaced with the OSS persona, so the public history runs consistently under one identity. - Replacing text in old commits —
git filter-repo --replace-text replacements.txt. A file with rules of the formold-string==>new-string, and filter-repo walks every file in every old commit. That’s how the codename disappeared from commit diffs, code comments, and old CLAUDE.md snapshots that were still lying around in the history. - Resetting branches and tags afterward — After filter-repo all the old commits have new SHAs, the history is effectively new. Before the first public push, that’s harmless: push it once cleanly, done. For an already-public repo you’d need
--force-with-lease, and all existing forks and clones would diverge. Hence the standard advice: do these history rewrites before the first public push if at all possible.
The tool itself isn’t included in standard Git — it’s a separate Python package: pip install git-filter-repo. Official successor to the old git filter-branch, which had so many footguns that the Git maintainers themselves point you at filter-repo.
Now I know. Back then: watched, waited, pushed. If you trust the tool, that’s a legitimate mode — the operation is deterministic, and before the first public push there’s no damage you can’t undo. If you don’t trust it, you should understand the commands before delegating, not afterward when blogging about it.
The More Critical Piece: the Session Directory
The more critical piece was the Claude Code session directory. The sessions are the source from which this blog series itself is being written — without them, no archaeological reconstruction of Parts 2 or 3. Claude Code stores JSONL transcripts in a path derived from the working directory — ~/.claude/projects/-home-user-projects-<directoryname>/. If I accidentally started Claude Code from the old directory after the migration, new sessions would land in the old path and would no longer be consolidated with the new session history. As a small guard against that: _archive/sync-sessions.sh, a helper script that mirrors the new session history into the updated path.
And then there was the tar backup: the entire old session directory archived as _archive/claude-sessions-<codename>-2026-04-25.tar.gz (2.3 MB). This archive is, incidentally, exactly what makes this blog series possible. The sessions from March 20, April 11, and April 25 — on which Parts 2, 3, and this article are built — would no longer be citable without this backup. The tar.gz lives in _archive/, is never committed to the repo, but is the source for everything that appears here as “quoted from the session.”
AGPL-3.0, Not MIT
With open-source projects, license selection often comes down to a quick trade-off: MIT when you want maximum adoption, GPL when you’re ideologically convinced, and something in between when you’re not quite sure what you want. That’s not a particularly useful framing.
The actual question for PullMD was: what happens when someone forks it, builds in modifications, and offers it as a cloud service — without sharing those modifications? With MIT the answer is: nothing. That’s the whole idea of permissive licenses. For a CLI tool or a library, that’s the right choice. Broad adoption, low friction, done.
For a self-hosted service the question is different. PullMD isn’t a passively consumed package — it’s a service that runs and answers requests. If someone runs it as a cloud service and builds in improvements I never see, I lose the incentive to understand what the actual problems in my codebase are. AGPL-3.0-or-later is precisely for this case: anyone who offers the service over a network and has made modifications must publish those modifications under AGPL.
That’s not an ideological statement about Free Software. It’s a question of incentive structure. CLI helpers, libraries, toolchain packages: MIT or Apache-2.0 are the right tools. Self-hosted services where network use is the primary channel: consider AGPL. The permissive dependencies deep in the stack — Express, cheerio, better-sqlite3 — remain freely choosable; license layering isn’t a binary either-or. More nuance on the consequences of individual licenses without a lawyer: choosealicense.com.
Making It Self-Host-Friendly
Up to this point the service ran only on my infrastructure, with my hostnames hardcoded in various places. For a public release that had to change.
The most important change was PUBLIC_URL as a configurable environment variable. The default value is https://${HOST_DOMAIN} — anyone who sets HOST_DOMAIN for Traefik automatically gets the correct URL. This variable is substituted on the /help page and embedded in /web-reader.zip, a generatable skill bundle for Claude Code. Without the substitution, self-hosters would see my hostname on the help page, not their own — a poor experience and an unnecessary dependency.
HOST_DOMAIN controls the Traefik label in docker-compose.yml. Anyone who wants to deploy this on their own domain sets that one variable, and Traefik handles routing and certificate issuance.
A clean .env.example as a template — the actual .env with OAuth credentials stays outside the repo. The README was completely rewritten for public consumption: no internal notes, no eval workspace, no briefing documents. .gitignore covers everything that shouldn’t go public: CLAUDE.md, SESSION_LOG.md, pullmd-improvements-briefing.md, docs/, .claude/, _archive/.
The Push
gh repo create AeternaLabsHQ/pullmd --public --source=. --remote=origin --push
The local initial-public-release commit was ready on April 25 at 18:12. But two more days of polish passed before the push actually ran — v1.0.0 and v1.0.1 were tagged locally but not pushed. v1.0.1 was a Reddit comment-cache bugfix that needed to ship to my Ubuntu Docker host before the repo went public. On April 27 at 09:32 in the morning, gh repo create finally ran — v1.0.2 is the first tag in the public history. In the days that followed came small UI polish commits — footer, version templating, help page layout. Those are the normal last-touch things that only become visible when you look at your own service with real distance for the first time.
A tool that began in March as a Reddit toy was, eight weeks later, officially open source — for any URL, at github.com/AeternaLabsHQ/pullmd.
What I Actually Have Now
A running HTTP service on its own domain that I use every day — for Reddit posts, for articles, for documentation pages, for anything I want to bring into a chat context as clean markdown.
Underneath: three extraction paths with a quality score, an MCP server for Claude Code, a generatable skill bundle, a PWA for the Android share workflow. P3.1 has been running since April 25 — a SQLite table with an extraction log, a /api/stats endpoint. One week of data collection, then I decide whether P3.2 (a Playwright microservice for JS-only sites) is worth it or would be feature creep. Both could be useful. Whether they’re necessary is what the stats endpoint will show.
The project is public on GitHub, AGPL-3.0, README written for others. And there’s this blog series that traces the arc from a frustrating Ctrl+A moment to an OSS release — with real sessions as source documents, not as reconstructed retelling.
What started as Ctrl+A frustration is, eight weeks later, a piece of software I use daily. With or without open source — it was worth the effort.
Part 4 of 9 in the PullMD series.
← PullMD Part 3: Beyond Reddit, Beyond Readability | PullMD Part 5: Reddit Day and First Issues →