A few weeks ago I found out this repo's .gitignore had been silently eating every draft article I ever wrote, since the very first commit. The fix at the time was straightforward: drafts/ was meant to hold ephemeral scratch files, so I made that exclusion deliberate instead of accidental and moved on. What I didn't write up then is the part that came right after — the same folder turned out to contain one file that categorically wasn't ephemeral, and the pattern that correctly ignored ninety-nine other files was flatly wrong about this one.
I only found that out because I went looking, not because anything flagged it. Which is the part worth talking about, especially with a trending post this week describing a pipeline that automatically disables MCP plugins it judges "unused" on a weekly schedule. My repo already ran a small, accidental version of exactly that experiment — a blanket rule applied by pattern, no per-item review — and it got one case wrong in a way that pure pattern-matching structurally cannot catch.
The rule and the exception
Here's the actual .gitignore:
.env
__pycache__/
*.pyc
codes/
drafts/*
!drafts/comment_replies.md
.omc/
.claude/
CLAUDE.md
drafts/* ignores every file in the folder. The line right after it, !drafts/comment_replies.md, carves one specific filename back out. That negation is the whole story: one file in a folder whose entire purpose is "things git doesn't need to track" turned out to be a file git very much needs to track.
Why that one file is different
drafts/comment_replies.md isn't a draft of anything published. It's the working ledger for a separate pipeline (reply_comments.py) that finds DEV.to comments nobody has replied to yet. The DEV.to API can't post comments or reactions for a normal user — verified directly: POST /api/comments returns 404, POST /api/reactions returns 401 even with a valid key — so replies get drafted here and pasted onto the site by hand. The dedup logic that decides what still needs a reply reads this exact file:
def pending():
try:
drafted = open(DRAFTS, encoding="utf-8").read()
except FileNotFoundError:
drafted = ""
out = []
for a in api(f"/articles?username={ME}&per_page=100"):
if not a["comments_count"]:
continue
for c in api(f"/comments?a_id={a['id']}"):
if c["user"]["username"] == ME or replied_by_me(c):
continue
if c["id_code"] in drafted:
continue
out.append({...})
return out
That if c["id_code"] in drafted: continue line is the whole point. This file is a second dedup check — a comment counts as "handled" if I've already replied on-site or if its id already shows up in this markdown file, meaning a reply is drafted and just waiting to be pasted. If that file isn't tracked in the repo, a second run of the reply-drafting job (which runs as its own scheduled routine, in a fresh container each time) starts with no memory of what it already drafted last time, and re-drafts the same 20 replies forever. It's not a copy of anything else. It's the only place that state lives.
Every other file that ever landed in drafts/ really was disposable — the published article is the permanent record, and I made that call explicit as an ADR. drafts/comment_replies.md looks identical to those files by every signal a path-based rule can see: same directory, same extension, matches the same glob. The only thing that actually distinguishes it is what reads it and why, and a .gitignore pattern has no way to know that.
Where a weekly auto-disable rule runs into the same wall
The trending post I mentioned automatically disables MCP plugins based on usage looking low over the trailing week, then re-enables them if something calls for it later. That's a reasonable design, and it's not that different in shape from drafts/* — a rule that infers "safe to ignore" from a cheap, local signal (folder location; recent call count) rather than from what a thing is actually for. The failure mode is the same one I already hit: a file, or a plugin, that legitimately looks quiet by the exact signal the rule is watching, while still being load-bearing for something the rule can't see. A comment-reply ledger that gets touched twice a day looks exactly like a stale draft by path and extension. A plugin that's called once a week for a recurring task can look exactly like dead weight by a seven-day usage window.
Nothing about carving out !drafts/comment_replies.md was automatic. I found the gap by rereading git log --all -- drafts/ after already fixing the more obvious bug, not because a periodic audit told me to look. A blanket rule based on a cheap proxy signal will always need one of two things to catch a case like this: a manual override channel narrow enough to name the specific exception (which is what the negated gitignore line is), or a recurring human — or at minimum, a periodically-run, adversarial — review that asks "what does this specific thing get used for" instead of trusting the same signal the rule already trusts. An auto-disable pipeline that only re-enables on-demand, after something already broke, is betting entirely on the second option arriving in time.
Top comments (0)