I open-sourced my personal blog repo so I could use Giscus for blog comments — it needs a public repo with GitHub Discussions enabled. But open-sourcing the repo meant everything was public: unpublished drafts, raw session notes, half-baked ideas, and my TODO list. For a thought leadership blog, that's a problem. People could just read GitHub instead of the site.
Before we even got to that realization, though, we found something worse.
All of the work in this session was done conversationally through Coder Agents on a self-hosted home lab setup. (You'll hear a lot more about that setup soon — I'll be writing about the full home lab build next.)
The Security Audit
First thing we did was scan the repo for anything sensitive now that it was public. Found three issues.
1. The .gitignore Was Gone
In a previous session, an agent had tried to set up a drafts workflow. The idea was to use .gitignore to keep drafts out of the repo. When that broke persistence (gitignored files don't survive workspace destruction), I asked the agent to fix it. Instead of removing the one blog-drafts/ line, it replaced the entire .gitignore with a single comment — deleting all 50 standard Next.js ignore patterns.
This meant .env, .env.local, node_modules/, .next/, .vercel/, *.pem — none of it was being ignored. If anyone (or any agent) had run git add ., every secret in .env would have been committed to a public repo.
The root cause: The agent conflated two separate concerns — git tracking (persistence) and site publishing (visibility). .gitignore controls what git tracks, not what the site renders. The blog already had published: false frontmatter support in posts.ts that filters unpublished posts from the public site. The agent didn't look at existing code before reaching for a filesystem-level solution.
Lesson: When an AI agent suggests a fix, check whether the codebase already solves the problem. Also, always diff what an agent changed — don't assume a targeted edit was actually targeted.
2. Live Server URL Exposed
The blog drafts contained my actual Coder server tunnel URL — a live endpoint to my self-hosted instance sitting in the blog fodder notes from a previous session. Anyone could have tried to hit it.
Fix: Replaced with a placeholder and rotated the URL.
3. Infrastructure Reconnaissance
The drafts also contained hardware specs, home lab architecture details, systemd config paths, and multi-user setup info. Not a vulnerability per se, but useful reconnaissance for someone targeting the setup.
Verdict: Acceptable for a "building in public" blog, but worth being aware of.
The Real Problem: Code vs. Content Visibility
After fixing the immediate issues, we hit the bigger question: what about a world where this is a well-trafficked site? Anyone could ignore the site entirely and browse GitHub for unpublished drafts, upcoming topics, and editorial strategy.
published: false only gates the rendered site. GitHub shows everything.
Why Not Just Make the Repo Private?
Giscus. The whole reason we open-sourced was for blog comments. Giscus requires a public repo with GitHub Discussions enabled. Making the repo private kills comments.
The Key Insight
Giscus doesn't care what's in the repo — it just needs a public repo to host GitHub Discussions. The discussions are completely independent of the repo's file contents. So we could separate the code from the content without touching Giscus at all.
The Solution: Two Repos
the-vibe-coder (public) — The blog engine. All source code, configs, components, API routes. Giscus stays pointed here. Open source, as intended.
the-vibe-coder-content (private) — All content: published posts, unpublished drafts, raw session notes, settings, images, TODO list. Nobody sees this but me.
How They Connect
The critical design decision: the private repo uses the exact same directory structure as the original. This meant zero code changes to the GitHub API client, the post loader, or any admin panel routes. The only change was pointing the GITHUB_REPO environment variable at the private repo.
A prebuild script clones the private repo at build time and overlays the content into the working tree. On Vercel, this runs automatically before next build. Locally, you clone the content repo once and copy or symlink.
The Deploy Hook
Since Vercel watches the public code repo, it wouldn't know to rebuild when content changes in the private repo. A GitHub Action on the private repo hits a Vercel Deploy Hook on every push to main:
Content commit → GitHub Action → Vercel rebuild → site updated.
The Wiring
- Created the private content repo
- Pushed all content files (same directory structure)
- Added a
fetch-content.shprebuild script to the public repo - Updated
.gitignoreto exclude content directories - Removed content files from the public repo
- Updated
GITHUB_REPOon Vercel to point to private repo - Created a Vercel Deploy Hook + GitHub Action trigger
The Token Gotcha
First deploy failed with exit code 128 (git auth failure). The GITHUB_TOKEN was a fine-grained PAT scoped to only the original repo. Had to update it in GitHub to also include the new private repo. Fine-grained PATs don't automatically pick up new repos — if your build pipeline uses one and you add a new private repo, you'll get a 403 until you update the token's repo list.
What I Learned
Agents and .gitignore
Agents reach for .gitignore as a blunt instrument. When the problem is "don't show this on the site," the answer is almost never "don't track it in git." Those are different concerns:
- Git tracking = persistence, collaboration, backup
- Site publishing = what visitors see
Conflating them leads to either lost work (gitignored files vanish) or the opposite — a gutted .gitignore that exposes secrets.
Always Audit Before Open-Sourcing
We caught three issues in a five-minute scan. The .gitignore one was a genuine time bomb. Open sourcing without a security pass is shipping without testing.
Giscus Is Decoupled From Content
This was the unlock. You can have a public repo with zero content files and Giscus works perfectly. "I need Giscus" and "I need private content" aren't in conflict.
The Same-Structure Trick
By keeping the private repo's directory layout identical to the original, we avoided code changes entirely. The admin panel, the build process, and the content API all work unchanged — they just talk to a different repo via the same env var. This is the kind of thing that makes a migration smooth instead of a refactor.
By the Numbers
- 1
.gitignorerestored from 1 line to 52 lines - 1 server URL redacted and rotated
- 2 repos (1 public, 1 private)
- 0 code changes to the blog engine
- 1 prebuild script (24 lines of bash)
- 1 GitHub Action (8 lines of YAML)
- 1 Vercel Deploy Hook
- 1 fine-grained PAT updated
- 5 published posts confirmed rendering
- 1 unpublished draft confirmed hidden
- ~45 minutes from "is there anything sensitive?" to verified deploy
Top comments (0)