dev log_

Senior DevOps Engineer building in public. Shipped features across Local Fitness, Ghostwriter, and more — the tradeoffs behind them, and what broke.

Latest · Local Fitness · 6 min read read
· Local Fitness

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.

· Devlog

How 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.

· Devlog

When 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.

· Ghostwriter

A 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.

· Ghostwriter

Your 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.

· Devlog

npx 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.

· Devlog

Give 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.

· Devlog

The 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.

· Devlog

Building 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 Budget

Let 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 Fitness

Benchmarks 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 Fitness

Never 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.

· Resume

Let 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.

· Resume

An 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 Fitness

Giving 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 Fitness

Why 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.

· Ghostwriter

One 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.

· Ghostwriter

Build 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.

· Devlog

Turning '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 Fitness

Move 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 Fitness

Wiring 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 Fitness

Automating 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 Fitness

A 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 Fitness

Chaining 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.

· Ghostwriter

Parsing 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.

· Ghostwriter

A 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 Fitness

Live 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 Fitness

A 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 Fitness

A 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 Fitness

Build 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 Fitness

Why 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.

· Ghostwriter

One 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.

· Resume

Adding 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 Fitness

The 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.

· Devlog

Give 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.

· Devlog

What 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 Fitness

Give 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 Fitness

Reuse 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 Fitness

Make 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.

· Ghostwriter

A 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.

· Ghostwriter

Rendering 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.

· Devlog

Filtering 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.

· Resume

The 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 Fitness

Score 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.

· Ghostwriter

Score 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.

· Ghostwriter

A 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.

· Devlog

A 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.

· Devlog

Turning 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.