The bug report was simple enough: go install didn’t work. Not “didn’t work in some edge case” — just didn’t work, period, for anyone trying to install a tagged release. The kind of thing that makes you set down your coffee and start digging.

The culprit turned out to be colons. Specifically, colons in filenames. The ledger stores entries as JSON files under .timbers/, and the filenames were derived directly from ISO 8601 timestamps — complete with HH:MM:SS. Perfectly sensible naming on Linux and macOS. Completely illegal in Go’s module zip format, which the module proxy uses to serve tagged versions. Every release since v0.16.x had been quietly poisoned: the proxy couldn’t ingest the zip, so go install ...@latest just… failed.

This is a pattern I keep running into: the thing that works fine in your local workflow turns out to be hostile to an adjacent system’s constraints. Colons in filenames are valid on most Unix filesystems. They’re invalid in zip archives targeting Windows compatibility. Go’s module proxy enforces the zip constraint. Three systems, three different opinions about what a legal filename looks like, and the user catches the shrapnel.

The fix needed to thread a needle. The entry IDs themselves — those ISO 8601 timestamps with colons — are meaningful. They show up in CLI arguments, in JSON content, in anything that references an entry. Mangling the IDs would ripple everywhere. So the change was scoped tightly: only the filesystem encoding changes. IDToFilename and FilenameToID helpers now flip the HH:MM:SS separators to dashes for storage, and flip them back on read. The canonical ID stays clean. The files on disk become zip-safe.

The interesting decision was what to do about existing files. There were 146 entries in this repo alone, all with colons in their names. The tempting move was a hard cutover — migrate everything, drop legacy support, ship it. But forks exist. Downstream consumers exist. People running older binaries have written entries with colon-encoded filenames, and those entries live in git history forever. Forcing a lockstep upgrade felt wrong.

So instead: indefinite backward-compatible reads. The new code writes canonical dashed filenames, but legacyEntryPath provides a fallback that checks for the old colon-encoded name on reads. If you’re running a post-v0.18 binary against a repo that still has colon files — from a fork, from an old branch, from wherever — it just works. No migration required to read. For writing, the system transparently cleans up: when it writes an entry, it removes the legacy sibling if one exists. And for the impatient, timbers doctor --fix runs the bulk migration in one shot via FileStorage.MigrateLegacyFilenames.

The colon files in pre-v0.18 git history remain readable by post-v0.18 binaries forever.

That “forever” is doing real work in the sentence. It’s a commitment to not break time-travel. You should be able to check out an old commit, run the current binary, and have everything behave. Backward compatibility in a Git-native tool means compatibility across the entire history, not just the working tree.

The version bump to v0.18.0 rather than a patch reflects the reality: yes, the proximate cause was a bug fix, but the migration machinery in doctor --fix is a new capability. Users need to know something changed. The release was tagged against a tree with zero colon-encoded filenames, so the module proxy can finally do its job.

The takeaway is deceptively simple: know your deployment surface. A file that’s valid on your filesystem isn’t necessarily valid in the packaging format that delivers your software to users. The Go module proxy’s zip constraints aren’t exotic — they’ve been documented for years. But when you’re designing a storage layout, you’re thinking about your filesystem, not about every intermediary that will eventually touch those bytes. It’s the kind of thing you only learn by having it break in production, which is a polite way of saying someone filed an issue and I felt appropriately sheepish.

The install path works now. The old files are still readable. The new files are zip-safe. It’s the kind of fix that, when it’s done right, nobody should ever have to think about again.


This post was drafted with AI assistance from development log entries.