DEV Community

tiancaijb
tiancaijb

Posted on

I built a dev.to publishing tool for my coding agent. The key problem was the shell, not the code.

My coding agent runs most of my writing workflow now. Drafts live as markdown files, and publishing them to dev.to was the one manual step left. So I made it a tool the agent can call itself. One 8.9KB extension file. Three actions. Two environment traps that had nothing to do with the code.

This is part of a build-in-public experiment that started in August 2026. I ran 43 sites to almost $0 and ~320 monthly visits before realizing the lesson: dev.to gives more traction in 48 hours than 25 static sites got in a month. Publishing fast and often is the whole game. The publish step has to be cheap.

What you need

  • A pi coding agent (any agent with an extension system works, the pattern carries over)
  • ~/.pi/agent/extensions/ as the extension directory
  • A dev.to API key, generated in your dev.to settings

The tool

pi auto-discovers TypeScript files in ~/.pi/agent/extensions/ and loads them with jiti, no build step. An extension is just a factory function:

import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "publish_devto",
    description: "Publish or update markdown drafts to dev.to, or list status.",
    parameters: Type.Object({
      action: StringEnum(["list", "create", "update"]),
      file: Type.Optional(Type.String()),
    }),
    async execute(_toolCallId, params) {
      // ...
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

The tool exposes three actions:

  • list — show what's in drafts/ and published/
  • create — read a draft, POST it to dev.to as a draft by default, write back devto_id/devto_url into the frontmatter, move the file to published/
  • update — PUT changes to an existing article using the stored devto_id

The design choice that matters: the markdown file is the source of truth. Frontmatter holds the title and the dev.to IDs. What you see in the file is what goes to dev.to, minus the bookkeeping keys.

The API key trap

The first version read the key from the shell environment. It worked in my terminal. It failed inside the agent session. The agent is launched by a terminal multiplexer, whose bash panes are non-interactive shells. My .bashrc has this as its sixth line:

[[ $- != *i* ]] && return
Enter fullscreen mode Exit fullscreen mode

That's a common early-exit guard: "if this shell isn't interactive, stop reading." Everything after it — including my export DEVTO_API_KEY=... — never runs in the agent's shell. The key simply wasn't there.

The fix was to stop trusting the environment entirely:

function devtoApiKey(): string | null {
  const fromEnv = process.env.DEVTO_API_KEY;
  if (fromEnv) return fromEnv;
  try {
    return fs.readFileSync(KEY_FILE, "utf8").trim() || null;
  } catch {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Env var first, then a fallback to ~/.pi/agent/devto.key with chmod 600. The key never lives in code, and the tool works whether or not the shell bothers to load my exports.

Two more traps

401 that wasn't the key. GET /api/articles/me without a ?state= parameter returns 401 even with a valid key. That's the endpoint's behavior, not a credential problem. Use ?state=published or just test with a POST.

The write-back bug. create writes devto_id and devto_url back into the frontmatter of the archived file. My first pass got that bookkeeping wrong. A mock-API end-to-end test caught it before anything touched the real endpoint — worth the ten minutes to set up.

The workflow it enables

The agent now drafts, I review the file, publish_devto create stores it as a dev.to draft, and I hit publish in the browser. One human gate on the final click, everything else scripted.

The project is a public experiment in whether a solo developer can get to $1/day with a small agent budget. The machine that produces the content is also the content. This tool is one more brick, and the whole thing is on GitHub.

One more thing: this is the first extension the agent wrote entirely by itself. I gave the machine an evolution loop — it gathers skills from GitHub, writes its own extensions, and I review plus reload. This tool is the first output of that loop.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev

The shell problem is real. A lot of agent tooling looks fine in source code and fails at the launch boundary: cwd, env, PATH, quoting, credentials, and where stdout/stderr go. I like treating the publishing step as a product surface, not just a script, because the edge cases are mostly operational.