dev log_
Senior DevOps Engineer building in public. Shipped features across Local Fitness, Ghostwriter, and more — the tradeoffs behind them, and what broke.
Let the model phrase the judgment, not derive it
My analysis tools were handing the LLM raw numbers and a legend, then trusting it to classify. Here is how I pulled every judgment into a pure, tested Python module so the model only has to write the sentence.
Let the model phrase the judgment, not derive it→
My analysis tools were handing the LLM raw numbers and a legend, then trusting it to classify. Here is how I pulled every judgment into a pure, tested Python module so the model only has to write the sentence.
· DevlogHow to turn an editorial rule into a lint rule→
I rewrote 38 blog posts against a writing contract and watched one rule get skipped over and over: every source in the bibliography must also be cited in the body. The rule was written down; nothing enforced it. Here's how to promote a style-guide sentence into a deterministic check your build can fail.
· DevlogWhen three releases land on one day, date alone can't sort your feed→
My dev-log feed sorted newest-first by date, which worked until the day I cut three releases before dinner and the newest one rendered at the bottom. The fix is a two-key comparator: date first, version as the tie-break, with the version segments compared as numbers. Here's how to build one that survives v0.10.0.
· GhostwriterA typo in a CSS class shouldn't need a human to catch it→
A rendered card and its stylesheet are two files that only agree by convention, nothing in HTML or CSS enforces it. Here's how to write a fast, deterministic test that catches the drift before a render silently ships unstyled.
· GhostwriterYour app's personal config doesn't belong next to your app's code→
Ghostwriter's voice profile and credentials lived inside its own repo, so a dev checkout and a marketplace-installed copy each needed their own setup. Moving personal config to the home directory collapses that back to one source of truth.
· Devlognpx ran my CLI and nothing happened→
Every npx invocation of my CLI exited 0 with no output, while running the file directly worked fine. The bug was one strict-equality check that can't survive npm's bin symlink. Here's how to detect 'am I the main module' correctly in an ESM CLI.
· DevlogGive the agent a plan, not a shell→
I rebuilt this dev log's skill so the LLM stops running twenty hand-rolled bash steps and starts consuming one JSON plan from a tested CLI. Here's the pattern: put everything deterministic in code you can test, and put everything non-deterministic behind a lint gate and a budget-capped judge.
· DevlogThe fallback that pointed at a directory that never existed→
devlog's voice-profile fallback pointed at a path its sibling tool never actually used, so the fallback could never fire and nothing ever errored. A small patch, and a useful lesson about tools that share files by convention.
· DevlogBuilding a Claude Code plugin marketplace that survives its own release pipeline→
A plugin marketplace's skill-discovery rule lives entirely in a directory shape, not a manifest field, so it has to be built, lint-checked, and released against that same shape. Here's how to do all three without a CI job that lies to you.
· Local BudgetLet an agent read your bank statements without handing it your PII→
A local MCP server turns your bank statements into tools an AI agent can call. Here is how to build one where the model gets answers, never raw account numbers.
· Local FitnessBenchmarks that count connections, not just milliseconds→
One of my fitness agent's MCP tools was opening nine SQLite connections to answer a single question. The fix was routine; this walkthrough builds the part that outlives it, a pytest-benchmark harness that asserts connection counts, compares against a committed baseline, and fails any PR that regresses either.
· Local FitnessNever let one LLM call sink a whole report→
My fitness agent's daily PDF gained a Claude-written coaching line, which put a network call in the middle of a synchronous render. Here's how to pair that call with a deterministic fallback, and how to prove the fallback works before the day you need it.
· ResumeLet the agent judge, let the script verify: a pattern for self-checking skills→
The resume skill's 1.0.0 deleted its own subprocess LLM call and let the invoking agent do the tailoring directly. The part worth stealing is what replaced the trust that used to live in that subprocess: a deterministic checkpoint plus a budget-capped judge, and the shape you can build for any skill that mixes judgment with facts.
· ResumeAn opt-in flag that can't quietly grow teeth→
Adding an opt-in escape hatch to a domain blocklist looks like a one-line change until you ask what happens the day someone else adds a second domain to that same list.
· Local FitnessGiving an MCP agent a way to refresh its own data→
My fitness agent could read the database but never freshen it. Here is how to add a side-effectful sync tool to an MCP server, keep it gap-aware, and keep it out of the loops that should stay read-only.
· Local FitnessWhy the container synced fine while the host kept hitting 429s→
The same sync code got rate-limited on the host and ran clean in the container, and the difference was an env-var fallback buried in a client library. How to make session caching explicit and pin it with a test so it can't silently regress.
· GhostwriterOne class, every token: theming a visual system with CSS custom properties→
Ghostwriter's post images moved from a dark look to a light one by changing a single CSS class. Here's how to build a token-driven theme switch, render it to a deterministic PNG, and catch the one missing token that makes text disappear.
· GhostwriterBuild a publish gate that fails closed, even when the input is garbage→
An agent that writes a confident claim will just as confidently skip checking it. Version 0.7.0 adds a code gate that blocks publishing until every outside-world claim traces to real, live sources, and it has to survive garbage input and a harness that lies about testing it.
· DevlogTurning 'cite your sources' into a contract an agent can't quietly ignore→
devlog 0.4.0 rewrote the instructions behind /devlog so every release post has to become a researched, cited implementation guide. The instructions alone don't guarantee it happens, so here's how to turn a soft citation ask into a structured contract and a checker that catches it when the model doesn't comply.
· Local FitnessMove the deterministic half of your LLM prompt into code→
When one prompt is doing eleven jobs, most of them have a right answer and don't need a model. Here is how to split an LLM feature into a tested planner, one toolless model call, and an advisory grounding check, then prove the cutover is safe before you flip it.
· Local FitnessWiring up CodeQL and dependency review before a repo goes public→
Before making a repo public, wire in the two security workflows GitHub gives you free: CodeQL for the code you wrote, dependency review for the code you pull in. Setup, verification with real output, and the traps that make both silently useless.
· Local FitnessAutomating the fix for squash-merge drift on a long-lived branch→
A squash-merged dev-to-main promotion leaves the two branches diverged, so the next promotion PR shows commits that already shipped. Here's a script and workflow that resets the branch automatically, including the branch-protection dance it has to do safely.
· Local FitnessA covered line and a tested line are not the same thing→
This release deleted a pile of confirmed-dead code and raised test coverage on the parts that were genuinely thin. The useful part wasn't the percentage, it was catching the tests that were only pretending to check something.
· Local FitnessChaining a release to CI's own success, not a raw push→
How to wire a GitHub release off a green CI run instead of a push, guard it so an already-shipped version can't double-release, and the default-branch trap that catches anyone wiring workflow_run for the first time.
· GhostwriterParsing the session logs Claude Code warns you not to parse→
Claude Code's own docs say its session transcripts are an internal format that can change on any release, yet discovering what you shipped recently has no sanctioned API. Here is how to read the logs anyway, defensively enough to survive the warning.
· GhostwriterA plugin registry made of file names, kept honest by one test→
Ghostwriter's new matrix card type shipped as one new file, with no registry code touched. Here's how a naming convention can stand in for a plugin registry, and the test that keeps it from shipping half-wired or quietly duplicated.
· Local FitnessLive MCP server instructions that survive a fresh clone→
An MCP server can hand its client a system-prompt hint at initialize. This build wires that field to a SQLite setting, resolves it fresh on every connect, and fails open when the read misses, so a config change lands without a restart and a fresh clone still starts.
· Local FitnessA prompt dial isn't a setting until code reads it too→
My fitness coach's brief had one hardcoded voice. Making it four selectable tones was the easy part; the real work was making sure a numeric 'harshness' dial did something a test could catch, not just something the model might notice.
· Local FitnessA settings resolver that always falls back to what already worked→
Five coaching preferences were hardcoded into a public repo as if they were universal truths. Turning them into user settings meant building a small resolver: one precedence order across three config layers, where every bad input falls back to the value that already worked instead of raising.
· Local FitnessBuild a status grader that never gets confused by today→
A training-plan feature graded today's finished workout as still pending, because the code asked the calendar instead of the outcome. Here's how to build a status grader that reads outcomes first, holds judgment only where the day is genuinely unsettled, and hands the UI one verdict it never has to second-guess.
· Local FitnessWhy your agent's tool needs to say pending, not missed, when data lags→
My fitness agent's plan tool needed to tell the difference between a workout that failed and one my Garmin sync simply hasn't caught up to yet. Here's how to build a single agent tool that grades real-world state against a data freshness boundary instead of the calendar.
· GhostwriterOne stylesheet, many card types: a themeable render pipeline with CSS variables and Playwright→
Ghostwriter's image cards got pulled onto one shared base and grew a portrait carousel with a progress bar. The mechanism behind it, CSS custom properties feeding a headless-browser render pipeline, is a pattern you can reuse for any HTML-to-image or HTML-to-PDF generator.
· ResumeAdding a free LLM judge to your build, without trusting it blindly→
The resume skill's v0.2.0 benchmark scores its output with two LLM judges that run through the Claude CLI instead of the metered API, for zero marginal cost. Making that safe took a timeout that actually kills the child process, a wrapper that fails open instead of crashing the build, and a hard line between what the judge gets to block and what it only gets to report.
· Local FitnessThe setting that fixed a slow AI brief was hiding behind one that silently did nothing→
A Claude-generated daily fitness brief went from about four minutes to under 90 seconds, but not from the parallel rewrite I designed. Here's how to gate a fix behind a kill criterion, find out which agent settings are real versus silently inert, and confirm the trade with a blind judge instead of a hunch.
· DevlogGive your CLI script two lives: a program and a module→
devlog's v0.3.1 test suite needed bin/devlog.js's logic importable without also running its command-line dispatch. Here's the guard pattern that split the two, and the symlink trap that broke it again a few weeks later.
· DevlogWhat it takes to treat a git tag as untrusted input→
devlog switched from a daily commit log to one entry per release tag. Building that meant treating every tag name as untrusted input, picking the right previous release even when prereleases are mixed in, and making the whole run safe to repeat.
· Local FitnessGive your MCP server a write tool, then delete the model from it→
Once an MCP server can write back as well as read, the model has no reason to run inside it anymore. Here's how to build that write tool, and the exact way a 'read-only' guard fails if you enforce it with a keyword check instead of at the connection itself.
· Local FitnessReuse your Claude Agent SDK tool server as a standalone MCP endpoint→
I exposed local-fitness's in-process Claude agent tools to real MCP clients over HTTP and stdio without writing a second copy of them, then closed the three gaps a network-reachable localhost server actually has that a subprocess-launched one doesn't.
· Local FitnessMake the agent's only write a draft, and let the database enforce the rest→
I gave an AI agent its first real database write path by making the dangerous action unreachable instead of trusting it to behave: the tool schema never accepts a status field, and a partial unique index backstops the single-active-row rule even if the application code has a bug. Here's how to build the same draft-only boundary for any agent that writes to your own data.
· GhostwriterA PDF is a slide deck LinkedIn will swipe through for you→
ghostwriter v0.3.0 added carousel support: a headless-browser render step that turns HTML slides into a single PDF, and a publisher path that uploads it through a document API LinkedIn renders as a swipeable carousel. Here's the render-and-ship pipeline, built generic enough for any chunked document upload.
· GhostwriterRendering HTML cards to PNG with a headless browser, and why the bars don't have to be to scale→
Ghostwriter's 0.1.0 release added one new card type: three ascending bars for an accelerating progression, with the bar heights fixed for effect rather than computed from the numbers next to them. Building it is a clean example of a small, reusable system: a CSS brand guide, a headless-browser render step, and a verification step that checks the honesty claim is actually true.
· DevlogFiltering a monorepo's git log without trusting the filter→
v0.2.0 let one devlog config serve several projects out of a shared monorepo. Getting there safely is a short lesson in two habits: scoping git history correctly, and treating any value headed for a shell command as input you don't trust yet.
· ResumeThe thinking tokens a CLI subprocess never needed to spend→
A résumé-tailoring skill that shells out to the claude CLI was burning 11-15k thinking tokens on an 800-token JSON answer. The fix was one environment variable, a validation step that moved out of the prompt and into code, and a sweep that proved the fix instead of assuming it.
· Local FitnessScore your prompt against the types your code already trusts→
I turned a personal Garmin-coaching script into real software: a pytest suite behind a coverage gate, CI wiring, and a scorer that cross-validates the agent's prompt against the typed contract its own output has to satisfy. Here's how to build that check for any prompt-driven agent.
· GhostwriterScore a prompt file like code, and give its glue full test coverage→
The ghostwriter skill's first versioned release made a markdown instruction file gradeable by CI and pushed its automation scripts to 100% line coverage. The two techniques worth stealing: a pass/fail scorer over frontmatter and body text, and monkeypatching a dependency your code doesn't even import until the function runs.
· GhostwriterA headless browser doubles as an image renderer, once you wait for the right signal→
Ghostwriter can now attach a locally rendered diagram or card to a post, and the two techniques worth taking are waiting on a real completion signal instead of network idleness, and keeping personal styling in a gitignored file a stranger's clone never has to see.
· DevlogA bare onClick isn't a button: making a shared React toggle keyboard-accessible→
The drop-in React component that renders a dev-log feed toggled each entry with a plain onClick, so every site that copied it inherited a feed keyboard and screen-reader users couldn't operate. Here's how to rebuild that toggle as a real ARIA disclosure control, and the test that proves the keyboard path works.
· DevlogTurning a personal script into a one-command npx installer→
Making a working personal tool installable by a stranger means declaring a real executable, prompting for defaults instead of demanding a config edit, and verifying the tarball before you trust a publish. Here's what that took, including the two ways npm quietly rejected the first attempt.