↓ Skip to main content
  1. Categories/

Development Environment

My 2 MB dotfiles repo grew to 31 GB. The cleaner found nothing to clean.

··1498 words·8 mins
My Mac mini froze hard enough that I held the power button to get it back. My first suspect was the macOS beta. My second was a terminal multiplexer I had been trialling. Both were wrong, and I had skipped the one check that would have pointed at the answer in about four seconds: the disk was full. My dotfiles repo tracks 2 MB of content, and it had grown to 31 GB. 26 GB of that was garbage, and two disk cleaners had already looked straight at it and reported nothing to clean.

A cask upgrade left the binary hanging. A copy of it started instantly.

A Homebrew cask upgrade on 2026-08-28 left my `claude` binary hanging forever on `--version`. No error, no exit, just a prompt that never came back. For two days nothing looked wrong, because every session already open kept working perfectly. The first person to notice was the first person to open a new one. The signature was valid. The bytes were intact. A byte-identical copy of the file, sitting in the same directory, started instantly while the original hung. That left exactly one thing it could be, and it was not the file.

The installer stamped an ID it never read. It reinstalled over itself.

··1376 words·7 mins
An installer writes a script. It stamps that script with `HERDR_INTEGRATION_ID=claude`, an identifier that exists for no reason other than to say "I made this". Then, to decide whether it has already run, it ignores that stamp entirely and compares the rendered command string in a config file. Rewrite the command to an equivalent form and the installer no longer recognises its own work, so it installs a second copy alongside the first. I hit three bugs in three days. They looked unrelated until I wrote them down next to each other: one in someone else’s tool, two in a plugin of mine. They share a shape, and the shape is worth more than any of the three fixes.

launchctl bootout returned 0. The label was still there. Bootstrap failed 5.

··1155 words·6 mins
`launchctl bootout` exited 0. `launchctl bootstrap`, on the very next line, failed with `Bootstrap failed: 5: Input/output error` because the label was still loaded. Then the bootout finished on its own schedule and the label went away, leaving a machine with no daemon at all and an installer that had just told me it was done. This one is mine: I caused it while fixing something else. 🧪 Tested with launchd · macOS I have written before about launchctl unload returning 0 while the daemon kept running, which turned out to be KeepAlive’s supervisor racing the teardown. This is the sibling. Same API, opposite failure, and where that one was launchd surprising me, this one I built myself.

The tunnel was up. The socket existed. Nothing was on the other end.

··1851 words·9 mins
The daemon reported the tunnel as established. The local socket file was right there on disk with the right permissions. The target sat at `offline` forever. Meanwhile `connect to /home/nick/.config/herdr/sessions/main/herdr.sock port 0 failed` was being written to a stderr stream that nothing in my process ever read, several times a second, for as long as I left it running. Deleting ClaudeDeck · Part 5 of 5 1 2 3 4 5 🧪 Tested with herdr 0.8.0 · macOS HerdDeck drives agents on a second machine. My laptop has the Stream Deck and the daemon; the desktop runs herdr and the agents actually doing work. They are joined by ssh -N -L <local.sock>:<remote.sock> host, a Unix-domain socket forward, and the daemon speaks the same protocol to a forwarded socket as it does to a local one.

The ring was 1.00:1 against its background. The key read 35% at 65%.

··1541 words·8 mins
A key on my Stream Deck showed an agent with a context window 65% full. The ring around it displayed a little over a third of a circle, which anyone would read as 35%. Not blank, not obviously broken, no missing-data placeholder. Confidently wrong, by exactly the amount that inverts the decision you are making when you look at it. Deleting ClaudeDeck · Part 4 of 5 1 2 3 4 5 🧪 Tested with herdr 0.8.0 · macOS That is the hardware, live against five agents on a remote machine. The top row is one key per agent. The tint is that session’s lifecycle state, and the ring is how full its context window is. Two independent signals, one key.

Five wrong answers in one day. One nearly deleted 10 GB of coursework.

··1903 words·9 mins
`mdls kMDItemLastUsedDate` returned `(null)` for Microsoft Word. I read the null as "never opened" and put Office on a removal list: 10.1 GB, four apps. One last check saved me. My home directory held 150 Office documents, a conference presentation edited two weeks earlier, and a PowerPoint lock file, which only exists while the file is open. The proof that the null was misleading had been sitting in my own diagnostic report for an hour. That was one of five. In a single day of hardening this machine, five different tools told me things that were not true. None of the answers looked like an error. Each one arrived as a clean, confident finding, and under each one a check had quietly failed or asked the wrong question. All five had the same shape underneath. Once I could name the shape, I stopped falling for it.

Three layers of secret defense for a public dotfiles repo. One was decorative.

··1190 words·6 mins
My dotfiles repo is public, which means any slip with a credential is permanent. History rewrites do not un-leak a key that a scraper already saw. So the defense cannot be one layer, and the interesting part of layering is not the count of tools. It is that each layer intercepts at a different moment: one before the commit exists, one at the moment of push, one sweeping the entire history in CI. The uncomfortable part, and the reason this post belongs to this series: one of my three layers used to be a decoration.

My commands vanished with exit 0. The culprit was a file named env.

··1441 words·7 mins
`env -u VAR command` did nothing. Exit code 0, no output, no error. A different command, same thing. A different variable, same thing. Any invocation that started with `env` just quietly evaporated. What finally made the problem visible was a `git init` that reported success while creating no `.git` directory at all. That was a year ago. The case closed last month, and the culprit was not uv, not some third-party installer, not anything exotic. It was this repo’s own bootstrap script. The thing that caught it was the regression test I had written for the original incident.

My dotfiles had a no-exceptions test gate. It had never run once.

··1331 words·7 mins
My dotfiles repo has a CLAUDE.md, and the CLAUDE.md has a rule in bold: every commit must pass the test suite, no exceptions. Within the first hour of an audit this July, I learned that this rule had been enforced exactly zero times since the day it was written. The hook file existed, its contents were correct, it even had its executable bit. It was just sitting at a path that yadm stopped reading a major version ago. No error message. No warning. To yadm, a hook in the wrong place and no hook at all are the same thing.

My diagrams rendered on refresh and vanished on click. The head never loaded.

··1010 words·5 mins
A reader clicking from my homepage to a post with a diagram got a block of raw mermaid source. The same reader pasting that post's URL directly got a rendered diagram. Same page, same build, same browser. The difference was the click, and the bug had been live on this site for months across every diagram, every math formula, and every chart, because I had only ever tested pages by loading them directly. 🧪 Tested with Blowfish 2.10x · htmx 2.0.10 Two features, both reasonable, one collision # This site has htmx’s hx-boost on the body: internal navigation swaps page content in place instead of doing full page loads, which keeps transitions smooth. Separately, the Blowfish theme is smart about heavy libraries: mermaid, KaTeX, and Chart.js bundles are only included on pages that use them, injected into the <head> of exactly those pages.

My blog publishes one post a day. I haven't touched the deploy in weeks.

··1104 words·6 mins
In July I went three weeks without opening my blog repo. During those weeks it published two posts, on schedule, each one confirmed live by an automated check, and the only reason I know all this is a green history in the Actions tab. The system's single notification channel is a failure email, and it has never fired. This post is the full pipeline, including the parts that only exist because something went wrong on the way here. 🧪 Tested with Hugo 0.164.0 · Cloudflare Pages The one Hugo fact everything hangs on # Hugo skips content dated in the future unless you pass --buildFuture (docs). That single default turns the date field into a release valve. Merge a post dated next Tuesday and production simply does not contain it: not in the sitemap, not in RSS, not at its URL. It sits in main, invisible, until a build happens after its date.