I went looking for "the dev.to publish script" in this repo and found two of them. Not two versions in git history — two live, executable, fully-working .py files sitting in the same directory: post_article.py and publish_devto.py. Both authenticate with the same DEV_TO_API key, both POST to the same https://dev.to/api/articles endpoint, both would run without error if invoked. Only one of them does what the current workflow actually needs.
That's a specific, boring-sounding kind of bug, and I want to walk through why it's more dangerous for an agent than for a human maintainer, because the failure mode is different.
What each one actually does
post_article.py is the older script. It reads a single hardcoded file, article_draft.md, strips the leading # H1 as the title, and posts it with a fixed tag list baked directly into the source:
payload = json.dumps({
"article": {
"title": title,
"body_markdown": body_md,
"tags": ["git", "python", "ai", "devtools"],
"published": False, # draft — review before publishing
}
}).encode()
published: False is hardcoded. The tags are hardcoded. There's no argument parsing — it only ever knows about one file, article_draft.md, sitting next to it.
publish_devto.py is what replaced it. It takes a path argument, parses YAML-ish frontmatter for title, tags, and published, strips an optional leading H1 as a fallback, and posts whatever the frontmatter says:
tags = [t.strip() for t in meta.get("tags", "").replace(",", " ").split() if t.strip()][:4]
published = meta.get("published", "false").lower() in ("true", "1", "yes")
It also carries two fixes the old script never got. First, a User-Agent header — DEV.to's API 403s the default urllib UA on both GET and POST, and the fix is a single added line:
req.add_header("User-Agent", "Mozilla/5.0") # dev.to 403s the default urllib UA
Second, an .env-loading helper that doesn't crash when no .env file exists, which matters in this sandbox specifically because DEV_TO_API is already set directly in the environment — no .env file is present at all. post_article.py's loader has no guard for that:
def load_env(path=".env"):
try:
with open(path) as f:
...
except FileNotFoundError:
pass
post_article.py's version does have the same try/except FileNotFoundError, actually — that one got fixed at some point too. What it never got is the User-Agent header. Depending on exactly how DEV.to's bot-detection is tuned that day, running it could 403 outright, or it could succeed and silently create an unpublished draft with the wrong tags on whatever article_draft.md happens to contain at the time — which, months later, is not the article you meant to publish.
Why this bites an agent differently than a human
A human maintainer who hasn't touched this repo in a while opens it, sees two similarly-named scripts, and does what humans do: checks git blame, notices one is newer, checks the README, asks "wait, which one do I use?" That friction is annoying but it's a speed bump, not a landmine — it stops you before you act.
An agent working from a task like "publish this draft to dev.to" doesn't browse the directory and feel a twinge of suspicion at seeing two candidates. It searches for something that matches — a filename containing devto or article, a function that posts to dev.to/api/articles — and acts on the first sufficiently-good match it finds via grep or glob. Both files match "posts to dev.to/api/articles" equally well. Nothing in either file's content flags itself as the deprecated one; there's no error, no warning, no "don't use this" comment, because dead code doesn't announce that it's dead — it just sits there, fully able to execute, silently out of sync with however the workflow evolved since it was written.
That's the actual risk: not that the old script is broken, but that it isn't. A broken script fails loud. A superseded-but-functional script fails quiet — it runs, it returns 200, and the discrepancy (wrong tags, always a draft, no frontmatter support) only shows up later, if at all, when someone notices the draft that never should have existed or the published article with generic tags instead of the ones the content actually called for.
What actually fixes it
Not a comment. A comment says "deprecated" only to something already reading the file end to end, and an agent grepping for "how do I publish to dev.to" is matching on content and structure, not scanning for warning labels in files it hasn't decided to open yet.
The fix that actually works is deletion, or an unambiguous rename that removes the collision entirely. I moved the old behavior into git history where it belongs and left exactly one script that posts to DEV.to's API. There is no longer a wrong file to find, because there's no second file to find at all — the search space for "which publish script do I use" collapsed from a judgment call to a fact.
The broader habit I'm taking from this: when a script gets superseded, the replacement isn't done until the old one is gone. Keeping "just in case" copies around doesn't cost a human much — they route around it by convention and memory. It costs an agent a real chance of silently picking the wrong tool, because nothing about matching a search query distinguishes "the current way to do this" from "the way we used to do this before we fixed the UA bug."
Top comments (0)