<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://chameth.com/">
    <title>Chameth.com - posts like docker-automatic-nginx-proxy, migrating-from-github-to-forgejo but not g15-ram-upgrade, how-i-get-things-done, tailscale-docker-gotcha, why-you-should-be-using-https</title>
    <subtitle>Personal homepage of Chris Smith</subtitle>
    <link href="https://chameth.com/feeds/posts/like/docker-automatic-nginx-proxy,migrating-from-github-to-forgejo/unlike/g15-ram-upgrade,how-i-get-things-done,tailscale-docker-gotcha,why-you-should-be-using-https/" rel="self"/>
    <link href="https://chameth.com/"/>
    <icon>https://chameth.com/favicon.png</icon>
    <updated>2026-05-01T00:00:00Z</updated>
    <id>https://chameth.com/</id>
    <author>
        <name>Chris Smith</name>
    </author>
    <entry>
        <title>Monthly Meanderings: April 2026</title>
        <link href="https://chameth.com/monthly-meanderings-2026-04/"/>
        <updated>2026-05-01T00:00:00Z</updated>
        <id>https://chameth.com/monthly-meanderings-2026-04/</id>
        <content xml:lang="en" type="html">&lt;p&gt;It’s apparently been a whole month since &lt;a href=&#34;https://chameth.com/monthly-meanderings-2026-03/&#34;&gt;the last edition of Monthly Meanderings&lt;/a&gt;. Not sure when that happened. This month I’ve been to a LAN party, had an MRI, and have been trying unsuccessfully to get my doctor to change a prescription. The latter two are not doing much to help shake that middle-aged feeling I mentioned last month…&lt;/p&gt;
&lt;h3 id=&#34;website-updates&#34;&gt;Website updates&lt;/h3&gt;
&lt;p&gt;Another pair of blog posts for this month: &lt;a href=&#34;https://chameth.com/the-case-of-the-unchanging-config/&#34;&gt;The Case of the Unchanging Config&lt;/a&gt; about a problem I had with a bind-mounted config in a Docker container, and &lt;a href=&#34;https://chameth.com/migrating-from-github-to-forgejo/&#34;&gt;Migrating from GitHub to Forgejo&lt;/a&gt;, a write-up of… well… migrating from GitHub to Forgejo. That one’s been on my to-do list for a while, so it’s nice to finally get it written up.&lt;/p&gt;
&lt;p&gt;It’s not visible when you look at the site, but I’ve been doing a lot of refactoring this month. The existing design had a package for database operations, a package for the stylesheet, a package for shortcodes, and so on. As I bolt more and more things on this gets less and less useful: those packages get bigger and bigger, and trying to understand how, say, the music import functionality works means hopping all over the place. Instead, I’m moving towards having each feature contained in its own package. It defines its own stylesheet, database operations, HTTP handlers, and so on. They’re then all wired together at the top layer. There’s still some more to do on that front, but it’s progressing in a good direction, and I’m happier with the code.&lt;/p&gt;
&lt;p&gt;The only actual visible change is the addition of &lt;a href=&#34;https://chameth.com/wow/&#34;&gt;a page dedicated to World of Warcraft&lt;/a&gt; that automatically imports some data from the game’s API. We’ll come back to that later!&lt;/p&gt;
&lt;h3 id=&#34;other-projects&#34;&gt;Other projects&lt;/h3&gt;
&lt;p&gt;I made a trio of new tiny projects: &lt;a href=&#34;https://github.com/csmith/pdsps&#34;&gt;pdsps&lt;/a&gt;, a reverse proxy for a Bluesky/ATProto PDS that overrides the age verification response so you don’t need to hand over ID; &lt;a href=&#34;https://github.com/csmith/irc-adze&#34;&gt;irc-adze&lt;/a&gt;, a plugin for &lt;a href=&#34;https://github.com/greboid/irc-bot&#34;&gt;irc-bot&lt;/a&gt; that receives and announces &lt;a href=&#34;https://github.com/greboid/adze&#34;&gt;adze&lt;/a&gt; webhooks; and &lt;a href=&#34;https://github.com/csmith/wow-spec-switch&#34;&gt;wow-spec-switch&lt;/a&gt;, a super-simple addon for World of Warcraft that lets you switch specialisation with a command.&lt;/p&gt;
&lt;p&gt;I’ve also been working on a custom app launcher in the style of &lt;a href=&#34;https://ulauncher.io/&#34;&gt;ulauncher&lt;/a&gt; but without requiring WebKit to render things. I’ve not yet polished it up enough to open source it, but it’s not far off.&lt;/p&gt;
&lt;h3 id=&#34;entertainment&#34;&gt;Entertainment&lt;/h3&gt;
&lt;p&gt;Another single film month for me. It was a great one, though:&lt;/p&gt;
&lt;div class=&#34;film-review-parent&#34;&gt;
  &lt;section class=&#34;film-review raised-box&#34;&gt;
    &lt;img src=&#34;https://chameth.com/films/254/poster.jpg&#34; alt=&#34;Poster for Project Hail Mary&#34; loading=&#34;lazy&#34;/&gt;
    &lt;header&gt;
      &lt;h3 class=&#34;plain-header&#34;&gt;&lt;a href=&#34;https://chameth.com/films/project-hail-mary-2026/&#34;&gt;Project Hail Mary&lt;/a&gt;&lt;/h3&gt;
      &lt;div&gt;&lt;/div&gt;
      &lt;div title=&#34;9/10&#34;&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
      &lt;time&gt;2026-04-01&lt;/time&gt;
    &lt;/header&gt;
    &lt;div&gt;&lt;p&gt;A film adaptation of a book I loved that’s actually good. Huh!&lt;/p&gt;
&lt;p&gt;Ryan Gosling does great in what is often a solo act. Early on I thought the film was trying a bit too hard to be funny, but at some point it started working and I enjoyed it.&lt;/p&gt;
&lt;p&gt;There are lots of parallels to The Martian, Arrival, and so on, but it’s a sufficiently different mash-up of the concepts that it’s not a problem.&lt;/p&gt;
&lt;/div&gt;
  &lt;/section&gt;
&lt;/div&gt;
&lt;p&gt;I’m still enjoying &lt;a href=&#34;https://www.themoviedb.org/tv/288670-saturday-night-live-uk&#34;&gt;SNL UK&lt;/a&gt; a lot more than I expected to, although I keep forgetting what day it airs. They should put it in the name or something.&lt;/p&gt;
&lt;p&gt;The rest of my “entertainment” time has been spent playing World of Warcraft. I played an awful lot about 6-7 years ago, and have dipped in and out occasionally since. I got an MMORPG itch again recently and jumped back in, and I’m enjoying it a lot once again. I’ve been playing a healer properly for the first time (after enjoying healing in Final Fantasy XIV):&lt;/p&gt;
&lt;div class=&#34;wow-char-grid&#34;&gt;
&lt;div class=&#34;wow-char raised-box&#34; data-title=&#34;World of Warcraft Character Data&#34;&gt;
&lt;div class=&#34;overview&#34;&gt;
&lt;img src=&#34;https://chameth.com/wow/characters/Methrica.png&#34; alt=&#34;Methrica&#34; loading=&#34;lazy&#34;/&gt;
&lt;p class=&#34;detail&#34;&gt;Level 90&lt;/p&gt;
&lt;p class=&#34;detail&#34;&gt;Female Void Elf&lt;/p&gt;
&lt;p class=&#34;detail&#34;&gt;&lt;span class=&#34;wow-class-priest&#34;&gt;Holy Priest&lt;/span&gt;&lt;/p&gt;
&lt;p class=&#34;detail&#34;&gt;280 average item level&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&#34;info&#34;&gt;
&lt;h3 class=&#34;plain-header&#34;&gt;Methrica&lt;span class=&#34;realm&#34;&gt;-Terenas&lt;/span&gt;&lt;/h3&gt;
&lt;h4 class=&#34;plain-header&#34;&gt;Professions&lt;/h4&gt;
&lt;table class=&#34;plain-table professions&#34;&gt;
&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Midnight Alchemy&lt;/td&gt;&lt;td&gt;&lt;progress class=&#34;tier-progress&#34; value=&#34;100&#34; max=&#34;100&#34;&gt;&lt;/progress&gt;&lt;/td&gt;&lt;td class=&#34;tier-count&#34;&gt;100/100&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Midnight Engineering&lt;/td&gt;&lt;td&gt;&lt;progress class=&#34;tier-progress&#34; value=&#34;92&#34; max=&#34;100&#34;&gt;&lt;/progress&gt;&lt;/td&gt;&lt;td class=&#34;tier-count&#34;&gt;92/100&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Midnight Cooking&lt;/td&gt;&lt;td&gt;&lt;progress class=&#34;tier-progress&#34; value=&#34;1&#34; max=&#34;100&#34;&gt;&lt;/progress&gt;&lt;/td&gt;&lt;td class=&#34;tier-count&#34;&gt;1/100&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Midnight Fishing&lt;/td&gt;&lt;td&gt;&lt;progress class=&#34;tier-progress&#34; value=&#34;4&#34; max=&#34;300&#34;&gt;&lt;/progress&gt;&lt;/td&gt;&lt;td class=&#34;tier-count&#34;&gt;4/300&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;div class=&#34;links&#34;&gt;
&lt;a href=&#34;https://worldofwarcraft.blizzard.com/en-gb/character/eu/terenas/methrica&#34;&gt;View on blizzard.com&lt;/a&gt;
&lt;a href=&#34;https://raider.io/characters/eu/terenas/Methrica&#34;&gt;View on raider.io&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&#34;around-the-web&#34;&gt;Around the web&lt;/h3&gt;
&lt;h4 id=&#34;git-history-documentationhttpsgit-scmcomdocsgit-history&#34;&gt;&lt;a href=&#34;https://git-scm.com/docs/git-history&#34;&gt;git-history documentation&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;The latest release of git added this handy-seeming history subcommand. It allows you to reword a commit, or split a commit up interactively, without having to go through the interactive rebase song and dance.&lt;/p&gt;
&lt;p&gt;Git has acquired a bunch of new commands in recent releases, but I tend to ignore them in favour of the old ones I know well. This one will definitely get use from me, though.&lt;/p&gt;
&lt;h4 id=&#34;git-koans-by-steve-loshhttpssteveloshcomblog201304git-koans&#34;&gt;&lt;a href=&#34;https://stevelosh.com/blog/2013/04/git-koans/&#34;&gt;Git Koans by Steve Losh&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Sticking with the same theme, this series of koans about Git and its various quirks had me laughing. If you’re reading this blog, you’re probably part of the target audience for it.&lt;/p&gt;
&lt;h4 id=&#34;sqlite-prefixes-its-temp-files-with-etilqs-httpsaviimblag2026etilqs&#34;&gt;&lt;a href=&#34;https://avi.im/blag/2026/etilqs/&#34;&gt;SQLite prefixes its temp files with etilqs_&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I think I’d come across this before, but it’s still a fun anecdote. It reminds me of &lt;a href=&#34;https://daniel.haxx.se/email/&#34;&gt;Daniel Stenberg’s e-mail collection&lt;/a&gt;; as the author of curl his name is listed on basically everything with the inevitable influx of people contacting him.&lt;/p&gt;
&lt;h4 id=&#34;a-dot-a-day-keeps-the-clutter-awayhttpsscottlawsonbccompostdot-system&#34;&gt;&lt;a href=&#34;https://scottlawsonbc.com/post/dot-system&#34;&gt;A dot a day keeps the clutter away&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I love this approach to tracking how frequently things are used, and the weird sort of archaeological record you get from rotating the colours of the stickers used.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Migrating from GitHub to Forgejo</title>
        <link href="https://chameth.com/migrating-from-github-to-forgejo/"/>
        <updated>2026-04-30T00:00:00Z</updated>
        <id>https://chameth.com/migrating-from-github-to-forgejo/</id>
        <content xml:lang="en" type="html">&lt;p&gt;When Microsoft bought GitHub in 2018 my kneejerk reaction — like so many others — was to start looking for alternatives. For a while I self hosted a &lt;a href=&#34;https://about.gitea.com/&#34;&gt;Gitea&lt;/a&gt; instance but I never totally bought into it: some repositories I still pushed to GitHub, some I pushed to Gitea and they got mirrored, and I ended up causing myself problems when I got the two confused. Part of the problem was that the GitHub UI was faster and cleaner than Gitea’s at the time; using Gitea felt like a chore compared to GitHub. I ended up not maintaining it and eventually binning it and just going back to GitHub.&lt;/p&gt;
&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/migrating-from-github-to-forgejo/unicorn.png&#34; alt=&#34;A screenshot of the GitHub error page, featuring an angry-looking Unicorn.&#34; loading=&#34;lazy&#34; width=&#34;575&#34; height=&#34;477&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;An all-too familiar unicorn&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Fast forward eight years, and GitHub is about what we all imagined when Microsoft bought it. If you take the most pessimistic way of counting, they have &lt;a href=&#34;https://mrshu.github.io/github-statuses/&#34;&gt;zero nines of reliability&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. If you take the most generous, they have a single nine. That’s around 30 minutes of downtime every day. It feels like it must be more than that, given how many times you see the damned unicorn.&lt;/p&gt;
&lt;p&gt;Uptime aside, they’ve crowbarred LLMs in all over the place; they &lt;a href=&#34;https://github.com/actions/create-release/issues/119&#34;&gt;don’t have the time&lt;/a&gt; to maintain official GitHub actions; the ones they do maintain have &lt;a href=&#34;https://github.com/actions/toolkit/compare/09cb71a033743b7545c8a9181facc06d7d6012ba...7ae5c2f423367fd11aa625ddcc0bbb0a8e5de5fa&#34;&gt;Copilot running roughshod all over them&lt;/a&gt;&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;; actions themselves are slow to run&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;, often have weird transient failures, and there are &lt;em&gt;so many&lt;/em&gt; footguns that I’m pretty sure GitHub actions should be an entire category in the CWE top 10.&lt;/p&gt;
&lt;p&gt;Speaking of security, while I was drafting this post a remote code execution &lt;a href=&#34;https://www.wiz.io/blog/github-rce-vulnerability-cve-2026-3854&#34;&gt;was reported&lt;/a&gt; and the details are… impressive. The actual security issue was a pretty stupid oversight. Those happen. But they end up being able to execute code as a globally shared &lt;code&gt;git&lt;/code&gt; user with access to all the other repositories on the node. What? I can’t quite get my head around it. No sandboxing, no containers, it just runs as a &lt;code&gt;git&lt;/code&gt; user?&lt;/p&gt;
&lt;p&gt;So, yeah, I’m not a fan of GitHub in its current state. A few months back I took the plunge and set up &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt;, a fork of Gitea where they did radical things like add tests and ensure that it’s community operated not twisted for commercial use. Forgejo also potentially &lt;a href=&#34;https://dustri.org/b/carrot-disclosure-forgejo.html&#34;&gt;has some fun security problems&lt;/a&gt;&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;, but the way I host it mitigates more-or-less all problems. It’s also slightly less shocking when an open source project has lapses in basic security versus a $2 trillion corporation.&lt;/p&gt;
&lt;p&gt;It’s taken a bit of doing, but I’m very happy with the setup I have.&lt;/p&gt;
&lt;h3 id=&#34;the-basic-setup&#34;&gt;The basic setup&lt;/h3&gt;
&lt;p&gt;I run all my server software using Docker, and Forgejo is no exception. They have &lt;a href=&#34;https://forgejo.org/docs/latest/admin/installation/docker/&#34;&gt;decent documentation&lt;/a&gt; on how to get it running, and publish rootless images which is nice (and diminishes my urge to make an &lt;a href=&#34;https://chameth.com/artisanal-docker-images/&#34;&gt;artisanal version myself&lt;/a&gt;). You can set configuration options using environment variables, which is perfect for containers, although the names can end up a bit unwieldy. Like this thing: &lt;code&gt;FORGEJO__repository.signing__SIGNING_NAME=Chris Smith&lt;/code&gt;. It gets the job done, but the environment-to-ini mapping is pretty ugly.&lt;/p&gt;
&lt;p&gt;I didn’t want to expose Forgejo publicly, as I wanted to avoid having to try and secure it&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;, or dealing with bots scraping it, or users signing up, and so on. I wanted somewhere to store my git repositories, handle CI for me, and mirror them somewhere public for other people to see and interact with. If you’ve read some of my other posts you’ll probably guess where this is heading: &lt;a href=&#34;https://tailscale.com/&#34;&gt;Tailscale&lt;/a&gt;. Alongside the Forgejo container, I run a Tailscale instance with a &lt;code&gt;serve.json&lt;/code&gt; that covers both the web frontend and the SSH listener used for git operations:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;TCP&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;22&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;TCPForward&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;forgejo:2222&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;HTTPS&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Web&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Handlers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;Proxy&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;http://forgejo:3000&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;                &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;            &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;AllowFunnel&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;${TS_CERT_DOMAIN}:443&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This allows me to interact normally with Forgejo from any of my devices or servers (which all run Tailscale), while not exposing it to the Internet at all. It also handles the TLS certificates automatically, which saves me setting up something else to do that.&lt;/p&gt;
&lt;p&gt;For CI, I run the Forgejo runner image on the same server, and gave it a &lt;code&gt;dind&lt;/code&gt; container to use to actually run the workloads. &lt;code&gt;dind&lt;/code&gt; is the cutesy name for Docker-in-Docker, basically a Docker daemon running inside a Docker container. That keeps the CI workload separated nicely from the “production” workload running on the server, which is nice from both a security and a monitoring point of view.&lt;/p&gt;
&lt;h3 id=&#34;the-uncanny-valley-of-actions&#34;&gt;The uncanny valley of actions&lt;/h3&gt;
&lt;p&gt;Forgejo actions are basically like GitHub actions: they run a series of steps using one or more container images. You define those steps in the exact same clunky YAML format as with GitHub. The big difference between them is that GitHub defaults to using an absolutely massive base image filled with &lt;a href=&#34;https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md&#34;&gt;out-of-date preinstalled software&lt;/a&gt;, and Forgejo leaves that for the administrator to configure. You could, of course, just use the same image as GitHub, but lugging around a multi-gigabyte image that doesn’t even have recent versions of the software I want isn’t really my style. Instead, I set about using a plain &lt;code&gt;debian&lt;/code&gt; image as the base, and then did a face-palm when I tried to run a “normal” action and remembered they’re all written in JavaScript, so expect node to exist.&lt;/p&gt;
&lt;p&gt;Accommodating JavaScript isn’t really my style, either, so I did the very sensible thing of writing &lt;a href=&#34;https://github.com/csmith/actions&#34;&gt;my own suite of actions in Go&lt;/a&gt;. Being written in Go means they can be compiled statically, published as a container, and manage their own very limited dependencies. For example, the &lt;code&gt;checkout&lt;/code&gt; action is built into an &lt;code&gt;alpine&lt;/code&gt; container with &lt;code&gt;git&lt;/code&gt; added, while the &lt;code&gt;dockerbuild&lt;/code&gt; action uses the &lt;code&gt;buildah&lt;/code&gt; image as a base. Each image has the tools it needs, pinned to a recent version, and nothing more. This is very much not a sensible approach to take for most people, but it made me happy. With six basic actions, I could replace almost all the ad-hoc workflows I had in GitHub&lt;sup id=&#34;fnref:6&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:6&#34; role=&#34;doc-noteref&#34;&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Other than the base images and my self-inflicted JavaScript machinations, everything works the same as GitHub. But faster. So much faster. Most of my CI workflows finish on Forgejo before GitHub would even have allocated a runner to the job&lt;sup id=&#34;fnref:7&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:7&#34; role=&#34;doc-noteref&#34;&gt;7&lt;/a&gt;&lt;/sup&gt;. And I can actually view the logs without them glitching out.&lt;/p&gt;
&lt;h3 id=&#34;step-aside-dependabot-here-comes-renovate&#34;&gt;Step aside Dependabot, here comes Renovate&lt;/h3&gt;
&lt;p&gt;One of the boons and/or banes of hosting code on GitHub is access to Dependabot&lt;sup id=&#34;fnref:8&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:8&#34; role=&#34;doc-noteref&#34;&gt;8&lt;/a&gt;&lt;/sup&gt;. It’s a tool that monitors your dependencies, and automatically submits PRs when new versions are available. It also does security alerts, but they’re comically bad for Go at least&lt;sup id=&#34;fnref:9&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:9&#34; role=&#34;doc-noteref&#34;&gt;9&lt;/a&gt;&lt;/sup&gt;. I do like having dependencies automatically handled, especially with &lt;a href=&#34;https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns&#34;&gt;a cooldown&lt;/a&gt;, which is how I had Dependabot configured before moving.&lt;/p&gt;
&lt;p&gt;To replace it, I’ve configured &lt;a href=&#34;https://github.com/renovatebot/renovate&#34;&gt;Renovate&lt;/a&gt;, an open source alternative. I’d seen it used a bunch before: both by projects on GitHub who prefer it over Dependabot, and by people using GitLab and other platforms where Dependabot isn’t. It mostly does the same things, but Renovate has a few very nice extras.&lt;/p&gt;
&lt;p&gt;Firstly, it supports transcluding config from another repo. So in each of my many, many projects, I just have this stub of a renovate config:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;$schema&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;https://docs.renovatebot.com/renovate-schema.json&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;extends&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;local&amp;gt;meta/renovate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then the actual config lives in a &lt;code&gt;default.json&lt;/code&gt; in the &lt;code&gt;meta/renovate&lt;/code&gt; repository. I &lt;a href=&#34;https://github.com/csmith/forgejo-renovate&#34;&gt;mirror that repository&lt;/a&gt; to GitHub, if you want to have a look. This is an amazing feature. When I enabled cooldowns in my Dependabot config it was a &lt;em&gt;slog&lt;/em&gt; to go through all my active repositories and make the same change over and over again. With Renovate I can configure that centrally. As far as I know Dependabot can’t do anything like that, even for enterprises.&lt;/p&gt;
&lt;p&gt;Within that central config I use some of the other nice features. It has the ability to auto merge changes. I use this to automatically accept changes from trusted projects, or my own libraries:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchManagers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomod&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchPackagePatterns&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^github\\.com\\/csmith\\/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^chameth\\.com\\/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchUpdateTypes&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;minor&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;patch&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;automerge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;minimumReleaseAge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;0 days&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchManagers&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomod&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchPackagePatterns&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^github\\.com\\/csmith\\/&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;^chameth\\.com\\/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;matchUpdateTypes&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;major&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;minimumReleaseAge&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;0 days&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This takes any minor or patch release of my libraries, and automerges it with no cooldown. For major version changes it overrides the cooldown but doesn’t automerge. To automerge Dependabot PRs you have to write an action, implement that logic yourself, and hope you’ve successfully avoided all the footguns inherent in that.&lt;/p&gt;
&lt;p&gt;Speaking of major version updates, Renovate also has an option to automatically update the imports in Go packages when it’s offering a major update. In the config I have:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;postUpdateOptions&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomodTidy&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;gomodUpdateImportPaths&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Which means that every time it does an update it’ll run &lt;code&gt;go mod tidy&lt;/code&gt;, and it’ll update the import paths. In Go, &lt;code&gt;v2&lt;/code&gt; of a module has a different import path to &lt;code&gt;v1&lt;/code&gt; (so they can coexist if transient dependencies need different versions), so usually you have to go through and find and replace those paths. Renovate handles that for me now! I still have to address whatever breaking changes are in the new version, but there’s a lot less grunt work.&lt;/p&gt;
&lt;p&gt;It can also run custom commands&lt;sup id=&#34;fnref:10&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:10&#34; role=&#34;doc-noteref&#34;&gt;10&lt;/a&gt;&lt;/sup&gt;, so for example in my &lt;a href=&#34;https://github.com/csmith/legotapas&#34;&gt;legotapas&lt;/a&gt; project I have this extra snippet in the &lt;code&gt;renovate.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;postUpgradeTasks&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-err&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;commands&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;sh -c &amp;#39;rm plate_*.go&amp;#39;&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;go run ./cmd/generate&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;executionMode&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;branch&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    &lt;span class=&#34;chroma-nt&#34;&gt;&amp;#34;fileFilters&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;chroma-p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;chroma-s2&#34;&gt;&amp;#34;*.go&amp;#34;&lt;/span&gt;&lt;span class=&#34;chroma-p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  &lt;span class=&#34;chroma-p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I won’t go into the gory details, but this handles automatically regenerating a bunch of files that change every time a certain dependency is updated. Previously I had a separate action to do that on GitHub; just being able to drop commands in the config file is much nicer.&lt;/p&gt;
&lt;p&gt;To actually run Renovate, I simply have a workflow in Forgejo on a schedule that uses their official Docker image. It also triggers if I update the config, which is nice when I add a new rule, as it’ll immediately update the open PRs to comply. Overall, Renovate has felt like a pretty big quality-of-life upgrade.&lt;/p&gt;
&lt;h3 id=&#34;odd-problems&#34;&gt;Odd problems&lt;/h3&gt;
&lt;p&gt;Not everything has been smooth sailing. Sometimes when Renovate automerged a change, it would just… disappear. The PR was closed as merged, but the commit never landed on master. It wasn’t a huge issue as the next Renovate run would just recreate it, but it would then &lt;em&gt;disable&lt;/em&gt; the automerge and I’d get confused as to why. It turns out there was a major problem with how I had everything set up. Renovate was telling Forgejo to automerge when the required checks pass, but I had no branch protection rules set up in Forgejo, so as soon as the PR was created it was eligible to merge. As the CI was so fast I hadn’t noticed that the automerges were happening &lt;em&gt;before&lt;/em&gt; the CI results were posted.&lt;/p&gt;
&lt;p&gt;The disappearing commits were because the repository was getting into a bad state when Renovate tried to automerge multiple changes instantly in the same repo. Forgejo has a bunch of maintenance commands that helped to fix that problem, and I then wrote a little script to enable branch protection rules so the CI would actually run. The automerging has been smooth since.&lt;/p&gt;
&lt;p&gt;Another issue I had early on was that merging would fail because the GPG keyring was locked. I had Forgejo configured to sign all the automatic commits, so you can verify what commits came from my infrastructure. Forgejo is the only process using the keyring, so I wasn’t sure why it was becoming locked. It turns out that GnuPG uses the hostname as part of the lockfile, and because Forgejo was running in Docker it was getting random hostnames based on the container ID. Explicitly setting a hostname made GnuPG and by extension Forgejo much happier.&lt;/p&gt;
&lt;h3 id=&#34;webhooks-and-updating&#34;&gt;Webhooks and updating&lt;/h3&gt;
&lt;p&gt;Forgejo has this amazing feature: you can add a webhook that’s called for all your repos. GitHub, for some reason, supports this for organisations but not people. Even though the two are treated the same in a lot of places. It doesn’t seem like it would be hard, and it’s such an obvious thing to want, but… no. I wrote a &lt;a href=&#34;https://github.com/csmith/webhooked&#34;&gt;whole tool&lt;/a&gt; to add webhooks for me to get around this. Now I don’t need it, Forgejo does the right thing!&lt;/p&gt;
&lt;p&gt;One particularly interesting use of these webhooks has been making my Docker containers auto-update when a new image is pushed. I previously had a webhook receiver for a few projects that would update them automatically, but GitHub package notifications were extremely laggy. Every other webhook fired within a minute or so of the event happening, but package webhooks would sometimes take 30-60+ minutes to fire. I opened a support case with GitHub and it got escalated to engineering and fixed about 10 weeks later&lt;sup id=&#34;fnref:11&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:11&#34; role=&#34;doc-noteref&#34;&gt;11&lt;/a&gt;&lt;/sup&gt;. It was better for a while, and then got worse again. Trying to do continuous deployment with a 30 minute random lag was frustrating.&lt;/p&gt;
&lt;p&gt;So now I had reliable webhooks, but the update process was a bit gross. I have a hangup about internet-facing services not having access to the Docker socket. It just feels like too big a security risk. So I had a process that received hooks, and then another that figured out if containers needed to be updated and restarted them. Adding a new container to the system meant recompiling the second process, which was a massive faff.&lt;/p&gt;
&lt;p&gt;Fortunately, I was saved from this mess by my friend &lt;a href=&#34;https://greboid.com/&#34;&gt;Greg&lt;/a&gt;. He wrote &lt;a href=&#34;https://github.com/greboid/adze&#34;&gt;adze&lt;/a&gt;, a tool that receives webhooks and uses Docker Compose to update matching containers. He wrote &lt;a href=&#34;https://greboid.com/ramblings/2026/04/Keeping-containers-up-to-date&#34;&gt;a blog post about it&lt;/a&gt;. It’s all one process, but that’s no longer a blocker for me because the webhooks can be delivered privately from my Forgejo instance without exposing adze to the internet. It finds matching containers automatically based on the images they’re using&lt;sup id=&#34;fnref:12&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:12&#34; role=&#34;doc-noteref&#34;&gt;12&lt;/a&gt;&lt;/sup&gt;, and combined with Forgejo not forcing me to add a webhook to every single repository makes my life so much easier. I write some code, push it to Forgejo, and within a minute or so it’s running on my dev instance.&lt;/p&gt;
&lt;p&gt;I have IRC notifications set up for when adze does updates, and I still have some old ones that announce GitHub package webhooks. I keep seeing things like this and chuckling:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;15:44:42 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: pending updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;15:44:45 &amp;lt;@ircjag&amp;gt; [ADZE] chameth.com: success updating git.yak-wall.ts.net/public/chameth.com
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;17:11:54 &amp;lt;@ircjag&amp;gt; [GHCR] Container pushed to csmith/chameth.com:dev.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I pushed a change at 15:43, Forgejo built it and pushed the image (to both its own registry and to GitHub), then at 15:44 adze updated the container to the new version. An hour and a half later GitHub gets around to delivering its own webhook.&lt;/p&gt;
&lt;h3 id=&#34;mirroring-and-prs&#34;&gt;Mirroring and PRs&lt;/h3&gt;
&lt;p&gt;The final piece I had to figure out was how to actually interact with people. Most of my projects are small and don’t get a massive amount of public contributions, but the option should still be there. And ideally not involve anyone having to figure out how to e-mail patches&lt;sup id=&#34;fnref:13&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:13&#34; role=&#34;doc-noteref&#34;&gt;13&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Mirroring outwards is simple: Forgejo supports it natively. Whenever I push to Forgejo, it pushes out to the corresponding repo on GitHub. I’ve also started mirroring some projects to &lt;a href=&#34;https://codeberg.org/&#34;&gt;Codeberg&lt;/a&gt;, to avoid having GitHub as my single “public” presence. But what happens when I get a pull request? If I merge it in the GitHub web UI, Forgejo will push its own branch over the top and get rid of it&lt;sup id=&#34;fnref:14&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:14&#34; role=&#34;doc-noteref&#34;&gt;14&lt;/a&gt;&lt;/sup&gt;. I could pull the changes, merge them, and push the result to Forgejo but then I don’t get the benefit of any CI.&lt;/p&gt;
&lt;p&gt;I ended up writing a small private tool that deals with this for me. I give it a GitHub PR, and it effectively imports it into Forgejo for me. I created a &lt;code&gt;prs&lt;/code&gt; organisation, and it’ll create a fork of the project in that org if it doesn’t exist. Then it pushes the PR contents to a branch, and creates a PR from the &lt;code&gt;prs&lt;/code&gt; repo into the main one. It runs as a user which can write to the PRs org, but only submit pull requests to public repositories outside of it.&lt;/p&gt;
&lt;p&gt;When the PR is imported, I can review it in the Forgejo UI, and then I can approve the CI workflows for that particular PR. This gives me some extra defence-in-depth in case anyone is trying to abuse one of those action footguns. Once the CI passes, I can merge it as normal in the Forgejo UI, it gets mirrored out to GitHub, and GitHub marks the PR as merged automatically. It’s not the most elegant solution, but the actual workflow is pretty smooth, especially for the low volumes of PRs I get.&lt;/p&gt;
&lt;p&gt;With that, I have Forgejo set up to do everything I need from a git host, and I’m no longer hamstrung every time GitHub goes down.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;This way of counting isn’t &lt;a href=&#34;https://evanhahn.com/in-defense-of-githubs-poor-uptime/&#34;&gt;particularly fair&lt;/a&gt;, but it’s somewhat realistic. GitHub break out their uptime for things like git operations, pull requests, actions, webhooks and packages. Realistically if any of those independent things are down then my overall workflow doesn’t, well, work. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;To be clear, I’m not objecting to the use of Copilot here. It’s a tool. But if you use a tool to do something fairly simple and it spews out 35 commits, including at least 5 reverts, then either it’s a terrible tool or you’re using it very wrong. I didn’t hunt down this example, I came across it while trying to debug something and wasted a bunch of time trying to figure out what on earth was going on. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;You could self-host runners, which may help, except they’ve already threatened to &lt;a href=&#34;https://github.blog/changelog/2025-12-16-coming-soon-simpler-pricing-and-a-better-experience-for-github-actions/&#34;&gt;charge you for the privilege of bringing your own runners&lt;/a&gt;. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;If people could stop finding RCEs in git services for a few days so I can stop redrafting this intro, that’d be great. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;I’m feeling pretty smug about this decision after reading about the security issues. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:6&#34;&gt;
&lt;p&gt;There’s one holdout: my &lt;a href=&#34;https://github.com/csmith/dockerfiles/&#34;&gt;dockerfiles repo&lt;/a&gt; which is a very special snowflake with a very complex workflow and I’m still deciding how to handle it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:6&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:7&#34;&gt;
&lt;p&gt;Which is to be expected, given I’ve got a server dedicated to one person, and they’re trying to serve however-many million users at once. That doesn’t make it any less nice, though! &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:7&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:8&#34;&gt;
&lt;p&gt;I’d link to it but there’s nowhere really good to link to. It’s just been subsumed into the ‘security’ functions of GitHub, along with a bunch of copilot nonsense. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:8&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:9&#34;&gt;
&lt;p&gt;The go project maintain a &lt;a href=&#34;https://pkg.go.dev/vuln/&#34;&gt;vulnerability database&lt;/a&gt; that includes details about which exact symbols are affected, and the &lt;a href=&#34;https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck&#34;&gt;govulncheck&lt;/a&gt; tool uses this to only report security issues in code that’s actually used. Dependabot doesn’t do that: it constantly alerts about code that’s not imported, so won’t ever be shipped. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:9&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:10&#34;&gt;
&lt;p&gt;In the default config it doesn’t allow anything of the sort, of course. You can whitelist individual commands, but because this is a private server I can merrily whitelist &lt;code&gt;.*&lt;/code&gt; and do whatever I like. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:10&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:11&#34;&gt;
&lt;p&gt;The turnaround time isn’t particularly impressive, but the fact that they kept the support case open, and the (human!) support agent updated me several times is. I don’t have a lot of good things to say about GitHub, but I don’t think I’ve ever had a support experience that good from a company of a similar size. Especially as I wasn’t a paying customer at the time. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:11&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:12&#34;&gt;
&lt;p&gt;Why didn’t I do that in the first place? It seems so obvious now… &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:12&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:13&#34;&gt;
&lt;p&gt;I really like the &lt;em&gt;idea&lt;/em&gt; of an e-mail based workflow, but I absolutely hate actually doing it. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:13&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:14&#34;&gt;
&lt;p&gt;This is what happened to me a lot when I was half using Gitea. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:14&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
    <entry>
        <title>Monthly Meanderings: February 2026</title>
        <link href="https://chameth.com/monthly-meanderings-2026-02/"/>
        <updated>2026-03-01T00:00:00Z</updated>
        <id>https://chameth.com/monthly-meanderings-2026-02/</id>
        <content xml:lang="en" type="html">&lt;p&gt;It doesn’t feel like a whole month has gone past since I wrote &lt;a href=&#34;https://chameth.com/monthly-meanderings-2026-01/&#34;&gt;the last instalment of Monthly Meanderings&lt;/a&gt;, even allowing for how short a month February is. For more context on this series, you can check out &lt;a href=&#34;https://chameth.com/monthly-meanderings-2025-12/&#34;&gt;the introduction to the first edition&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;website-updates&#34;&gt;Website updates&lt;/h3&gt;
&lt;p&gt;I only wrote one new blog post this month: &lt;a href=&#34;https://chameth.com/just-a-nod/&#34;&gt;Just a nod&lt;/a&gt;, which is about the “nod” button I added to the bottom of most pages. I did a lot of other little bits and pieces, though:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My blog posts are now automatically posted to &lt;a href=&#34;https://bsky.app/profile/posts.chameth.com&#34;&gt;a dedicated Bluesky account&lt;/a&gt;, and posts now include syndication links at the bottom.&lt;/li&gt;
&lt;li&gt;The site now tracks how long each request takes to generate along with how many SQL queries were used. You can see the details for the current request by hovering/tapping over the pi symbol at the bottom right of a page. To understand why it’s a pi symbol, you should watch &lt;a href=&#34;https://chameth.com/films/the-net-1995/&#34;&gt;The Net&lt;/a&gt;!&lt;/li&gt;
&lt;li&gt;Speaking of films, I redid the star rating rendering so instead of unicode stars it uses some hand-drawn pixel art stickers. They’re somewhat randomised, so they don’t look repetitive. This is what it now looks like:
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
.&lt;/li&gt;
&lt;li&gt;I spent some time tweaking various styles and layouts so everything works nicely on smaller screens. I try to make sure these things work when I make them, but it’s easy to miss a particular interaction, or make an innocent-seeming change that breaks tiny screens.&lt;/li&gt;
&lt;li&gt;I added a new page that shows stats about &lt;a href=&#34;https://chameth.com/walks/&#34;&gt;my walks&lt;/a&gt;, based on my Apple Health data. I had a private view of this for a while, but decided there’s no reason it couldn’t be more public.&lt;/li&gt;
&lt;li&gt;The &lt;a href=&#34;https://chameth.com/films/&#34;&gt;films page&lt;/a&gt; has gained a search box, to directly find my review of a particular film. I used &lt;a href=&#34;https://css-tricks.com/css-anchor-positioning-guide/&#34;&gt;CSS anchor positioning&lt;/a&gt; for the results panel. Modern CSS is fun!&lt;/li&gt;
&lt;li&gt;I fixed the &lt;a href=&#34;https://chameth.com/prints/&#34;&gt;3D prints&lt;/a&gt; page being utterly broken in numerous ways. It showed up on my shiny new “how many SQL queries does each page do?” dashboard, as it was doing multiple queries for each individual print instead of just getting all the data in one go. Then I noticed a badly written redirect rule had broken all the images and download links. Whoops!&lt;/li&gt;
&lt;li&gt;I did a lot of under-the-hood cleaning, most of which won’t be visible. Besides just making me happy, this means the contact form and nod buttons now work with Javascript disabled.&lt;/li&gt;
&lt;li&gt;Finally, I added a page that shows my &lt;a href=&#34;https://chameth.com/boardgames/&#34;&gt;board game plays&lt;/a&gt;. The data comes from exports from &lt;a href=&#34;https://www.bgstatsapp.com/&#34;&gt;bgstats&lt;/a&gt;. I have a bunch of ideas for other things to do with that data, but haven’t got around to it yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Phew. After writing all that out it feels like a really long month, not a short one!&lt;/p&gt;
&lt;h3 id=&#34;other-projects&#34;&gt;Other projects&lt;/h3&gt;
&lt;p&gt;Not much going on with my other projects. The only big thing was &lt;a href=&#34;https://github.com/csmith/goplum&#34;&gt;goplum&lt;/a&gt;, my self-hosted monitoring/alerting tool, getting a 1.0 release. It adds a reminder feature so you can get alerted repeatedly if a service is down. It also reworks how plugins work so they’re compile-time instead of trying to be clever and dynamic, as that never really gave any benefits.&lt;/p&gt;
&lt;p&gt;I’m still continuing my migration of projects to a private &lt;a href=&#34;https://forgejo.org/&#34;&gt;Forgejo&lt;/a&gt; instance. All of my personal repos are now sourced there, and mirrored out to GitHub, except one: my &lt;a href=&#34;https://github.com/csmith/dockerfiles&#34;&gt;Dockerfiles&lt;/a&gt;. I’m not sure how I want to approach that yet, as the action that does the updating is pretty involved. I also have some repositories split off into their own GitHub organisations which I haven’t got around to mirroring yet.&lt;/p&gt;
&lt;h3 id=&#34;entertainment&#34;&gt;Entertainment&lt;/h3&gt;
&lt;p&gt;Unlike the past few months, I didn’t touch many video games in February. The ones I’d been dabbling with didn’t really hook me enough to make me keep going back. I made up for it with lots of other media forms though. First up, a whole bunch of films:&lt;/p&gt;
&lt;ol class=&#34;watched-films&#34;&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/black-swan-2010/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/242/poster.jpg&#34; alt=&#34;Poster of Black Swan&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-02&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-net-1995/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/243/poster.jpg&#34; alt=&#34;Poster of The Net&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-03&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/office-space-1999/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/244/poster.jpg&#34; alt=&#34;Poster of Office Space&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-04&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-silence-of-the-lambs-1991/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/245/poster.jpg&#34; alt=&#34;Poster of The Silence of the Lambs&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-07&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/big-night-1996/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/246/poster.jpg&#34; alt=&#34;Poster of Big Night&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-10&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-cabin-in-the-woods-2012/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/247/poster.jpg&#34; alt=&#34;Poster of The Cabin in the Woods&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-11&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/knock-at-the-cabin-2023/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/248/poster.jpg&#34; alt=&#34;Poster of Knock at the Cabin&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-12&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/the-faculty-1998/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/249/poster.jpg&#34; alt=&#34;Poster of The Faculty&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-13&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/final-destination-2000/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/250/poster.jpg&#34; alt=&#34;Poster of Final Destination&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-13&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-half.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Half star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/get-out-2017/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/251/poster.jpg&#34; alt=&#34;Poster of Get Out&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-14&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;a class=&#34;plain-link raised-box&#34; href=&#34;https://chameth.com/films/no-country-for-old-men-2007/&#34;&gt;
      &lt;img src=&#34;https://chameth.com/films/252/poster.jpg&#34; alt=&#34;Poster of No Country for Old Men&#34; loading=&#34;lazy&#34;/&gt;
      &lt;time&gt;2026-02-17&lt;/time&gt;
      &lt;div&gt;
&lt;span class=&#34;star-rating&#34;&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-2&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-flat.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-1&#34;/&gt;&lt;img src=&#34;https://chameth.com/star.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Full star&#34; class=&#34;rot-0&#34;/&gt;&lt;img src=&#34;https://chameth.com/star-empty.png&#34; width=&#34;28&#34; height=&#34;28&#34; alt=&#34;Empty star&#34;/&gt;&lt;/span&gt;
&lt;/div&gt;
    &lt;/a&gt;
  &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I won’t write too much about them here — you can click through to read my reviews if you want — other than to call out the three excellent horror films I hadn’t seen before: &lt;a href=&#34;https://chameth.com/films/black-swan-2010/&#34;&gt;Black Swan&lt;/a&gt;, &lt;a href=&#34;https://chameth.com/films/the-cabin-in-the-woods-2012/&#34;&gt;The Cabin in the Woods&lt;/a&gt;, and &lt;a href=&#34;https://chameth.com/films/get-out-2017/&#34;&gt;Get Out&lt;/a&gt;. Not so long ago I would’ve said that I didn’t enjoy horror films, now they’re some my favourites.&lt;/p&gt;
&lt;p&gt;I don’t have any TV show tracking on my website (yet?), as I haven’t really watched much recently. This month I started watching &lt;a href=&#34;https://www.themoviedb.org/tv/250307-the-pitt&#34;&gt;The Pitt&lt;/a&gt; though, and it rapidly become one of my favourite shows. By “started watching” I mean “watched every aired episode of both season 1 and season 2”. I highly recommend it. The only problem is that it’s hard to find anything to watch afterwards, as nothing else can quite measure up.&lt;/p&gt;
&lt;p&gt;I’ve also been binging a book series: &lt;a href=&#34;https://www.amazon.co.uk/dp/B0DT7G27HH&#34;&gt;Beneath the Dragoneye Moons&lt;/a&gt;. It’s a LitRPG series: basically a fantasy setting that borrows systems from an RPG, like classes and levelling. Something about the genre really works for me, and Beneath the Dragoneye Moons is one of the more interesting series I’ve come across in the genre. I’ve been going through the series at the rate of about a book a day.&lt;/p&gt;
&lt;p&gt;Finally, board games! Nothing particularly notable, but now I can embed a pretty display of what I actually played:&lt;/p&gt;
&lt;ul class=&#34;played-boardgames&#34;&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Can&amp;#39;t Stop (1980)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/41/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/41/image.png&#34; alt=&#34;Box art of Can&amp;#39;t Stop&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;6 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Cartographers (2019)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/263918/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/263918/image.png&#34; alt=&#34;Box art of Cartographers&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;My Shelfie: The Dice Game (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/411543/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/411543/image.jpg&#34; alt=&#34;Box art of My Shelfie: The Dice Game&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Super Mega Lucky Box (2021)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/341530/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/341530/image.png&#34; alt=&#34;Box art of Super Mega Lucky Box&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;4 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;7 Wonders Dice (2025)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/446231/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/446231/image.jpg&#34; alt=&#34;Box art of 7 Wonders Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Let&amp;#39;s Go! To Japan (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/368173/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/368173/image.jpg&#34; alt=&#34;Box art of Let&amp;#39;s Go! To Japan&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Martian Dice (2011)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/99875/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/99875/image.png&#34; alt=&#34;Box art of Martian Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Pickomino (2005)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/15818/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/15818/image.jpg&#34; alt=&#34;Box art of Pickomino&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Space Base (2018)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/242302/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/242302/image.jpg&#34; alt=&#34;Box art of Space Base&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Terraforming Mars (2016)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/167791/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/167791/image.jpg&#34; alt=&#34;Box art of Terraforming Mars&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;2 plays&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Azul: Summer Pavilion (2019)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/287954/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/287954/image.jpg&#34; alt=&#34;Box art of Azul: Summer Pavilion&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Booty Dice (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/408658/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/408658/image.png&#34; alt=&#34;Box art of Booty Dice&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Castles of Mad King Ludwig (2014)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/155426/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/155426/image.jpg&#34; alt=&#34;Box art of Castles of Mad King Ludwig&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Coffee Rush (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/377061/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/377061/image.jpg&#34; alt=&#34;Box art of Coffee Rush&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Flip 7 (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/420087/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/420087/image.jpg&#34; alt=&#34;Box art of Flip 7&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Fluxx (1997)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/258/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/258/image.jpg&#34; alt=&#34;Box art of Fluxx&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Lodge (2026)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/452300/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/452300/image.jpg&#34; alt=&#34;Box art of Lodge&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Next Station: Paris (2024)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/403116/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/403116/image.jpg&#34; alt=&#34;Box art of Next Station: Paris&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Next Station: Tokyo (2023)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/380165/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/380165/image.jpg&#34; alt=&#34;Box art of Next Station: Tokyo&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Obsession (2018)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/231733/image.png&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/231733/image.png&#34; alt=&#34;Box art of Obsession&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
  &lt;li class=&#34;raised-box&#34;&gt;
    &lt;div class=&#34;image-container&#34; title=&#34;Ticket to Ride (2004)&#34;&gt;
      &lt;img class=&#34;background&#34; src=&#34;https://chameth.com/boardgames/9209/image.jpg&#34; aria-hidden=&#34;true&#34; loading=&#34;lazy&#34;/&gt;
      &lt;img class=&#34;foreground&#34; src=&#34;https://chameth.com/boardgames/9209/image.jpg&#34; alt=&#34;Box art of Ticket to Ride&#34; loading=&#34;lazy&#34;/&gt;
    &lt;/div&gt;
    &lt;p&gt;1 play&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these were online via &lt;a href=&#34;https://boardgamearena.com/&#34;&gt;Board Game Arena&lt;/a&gt;. I haven’t really had time for in-person games with all the TV, films and reading!&lt;/p&gt;
&lt;h3 id=&#34;around-the-web&#34;&gt;Around the web&lt;/h3&gt;
&lt;h4 id=&#34;start-all-of-your-commands-with-a-commahttpsrhodesmillorgbrandon2009commands-with-comma&#34;&gt;&lt;a href=&#34;https://rhodesmill.org/brandon/2009/commands-with-comma/&#34;&gt;Start all of your commands with a comma&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;An old post that was doing the rounds again on Hacker News. It’s an interesting idea. I recently aliased &lt;code&gt;cc&lt;/code&gt; to launch Claude Code with some custom parameters, and it horribly backfired when I tried to compile a C program and remembered that &lt;code&gt;cc&lt;/code&gt; was part of GCC. I haven’t transitioned all my personal commands to use a prefix yet, but I might.&lt;/p&gt;
&lt;h4 id=&#34;tactical-tornado-is-the-new-defaulthttpsolanodevblogtactical-tornado&#34;&gt;&lt;a href=&#34;https://olano.dev/blog/tactical-tornado/&#34;&gt;Tactical tornado is the new default&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;An interesting post about software developers who act as “tactical tornadoes”, and how that’s basically the role that all LLMs fill. Whenever I use coding agents, I often find myself following behind them in my IDE cleaning up bits and pieces. It’s often faster than prompting them to do it. I definitely get the “cleaning up after a tornado” metaphor.&lt;/p&gt;
&lt;p&gt;Amusingly, the agents will sometimes notice that you’re tidying up after them and ascribe it to an automated process, outputting something like “the automated linter hasn’t run yet” if you fall behind. There’s probably a deeper philosophical discussion about what it means when the agent thinks that the human is the automated part of the system…&lt;/p&gt;
&lt;h4 id=&#34;my-smart-sleep-mask-broadcasts-users-brainwaves-to-an-open-mqtt-brokerhttpsaimiliosbearblogdevreverse-engineering-sleep-mask&#34;&gt;&lt;a href=&#34;https://aimilios.bearblog.dev/reverse-engineering-sleep-mask/&#34;&gt;My smart sleep mask broadcasts users’ brainwaves to an open MQTT broker&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Another one for the “the ’s’ in ‘IoT’ stands for security” books. I find it hard to actually comprehend how these things get to market. Does nobody actually think about it, or do they know full well how bad it is and just launch it anyway?&lt;/p&gt;
&lt;h4 id=&#34;the-indieweb-and-small-webhttpschristianodevpostindieweb-smallweb&#34;&gt;&lt;a href=&#34;https://christiano.dev/post/indieweb_smallweb/&#34;&gt;The IndieWeb and Small web&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Ask three people to define ‘IndieWeb’ and ‘Small web’ and you’ll get about 20 different answers. This post by Christiano almost exactly nails my own views on the terms, and explains them well. I think most personal sites (my own included) straddle both terms to some extent, but having the labels is useful in discussions.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Monthly Meanderings: December 2025</title>
        <link href="https://chameth.com/monthly-meanderings-2025-12/"/>
        <updated>2026-01-01T00:00:00Z</updated>
        <id>https://chameth.com/monthly-meanderings-2025-12/</id>
        <content xml:lang="en" type="html">&lt;p&gt;For a while I’ve been idly thinking about a way to get smaller bits of content
onto my website without it being too annoying for me, or too hard to consume. Things
like interesting links, small project updates, and so on.&lt;/p&gt;
&lt;p&gt;I didn’t immediately come up with anything I was happy with, so just sat on it.
Ignoring the problem seems to work surprisingly well for things like this.
I’d been meaning to redesign the &lt;a href=&#34;https://chameth.com/about&#34;&gt;about page&lt;/a&gt; for a while, but wasn’t
sure what to do with all the interests I wanted to list. After ignoring the
problem for a while I came across the &lt;a href=&#34;https://chrisburnell.github.io/interests-directory/&#34;&gt;interests directory&lt;/a&gt;
and realised I could just bundle all those things into their own page.&lt;/p&gt;
&lt;p&gt;This seems amazingly obvious in hindsight, but I think I needed a bit
of a push to accept that a page just about my interests was… well…
interesting. Anyway, as the end of December rolled around, I got a similar
dose of inspiration: a flurry of monthly round-up posts from various blogs
I follow. If I bundle up all the small bits and bobs, it’ll make something
vaguely blog post shaped, and writing and consuming those is already a
solved problem. Again, pretty obvious in hindsight, and again I think I
just needed a nudge to shut up the little voice in my head saying “but those
people all have interesting things to say… do you, really?”&lt;/p&gt;
&lt;p&gt;So without further ado, welcome to the inaugural post in my new monthly
meanderings series. If you have any feedback on the format or contents,
please drop me a note with the contact form at the bottom!&lt;/p&gt;
&lt;h3 id=&#34;website-updates&#34;&gt;Website updates&lt;/h3&gt;
&lt;p&gt;As already mentioned, I reworked the &lt;a href=&#34;https://chameth.com/about&#34;&gt;about page&lt;/a&gt; and split out
a new &lt;a href=&#34;https://chameth.com/interests&#34;&gt;interests page&lt;/a&gt;. The old about page had a few lists
of interests show up in thought bubbles when you hovered over things,
which was a cool effect on desktop, but barely worked at all on mobile.
The new page gives me a lot more room for waffle, and I do like waffling.&lt;/p&gt;
&lt;p&gt;Only a single new blog post this month:
&lt;a href=&#34;https://chameth.com/exposing-game-servers-over-tailscale/&#34;&gt;exposing game servers over Tailscale&lt;/a&gt;.
Just a quick how-to I wrote up, having not found it documented usefully
anywhere before. I have a bunch of ideas for posts I want to write, but
none of them have really sparked joy quite yet, so I’m once again employing
the “sit on them for a bit and maybe inspiration will strike” approach.&lt;/p&gt;
&lt;h3 id=&#34;other-projects&#34;&gt;Other projects&lt;/h3&gt;
&lt;p&gt;I created a small collection of Docker related projects this month:
&lt;a href=&#34;https://github.com/csmith/containuum&#34;&gt;containuum&lt;/a&gt; is a go library that
helps with listing containers, and keeping that list up-to-date. On top
of that I built &lt;a href=&#34;https://github.com/csmith/centauri-docker-confd&#34;&gt;centauri-docker-confd&lt;/a&gt;
which generates a config for &lt;a href=&#34;https://github.com/csmith/centauri&#34;&gt;centauri&lt;/a&gt;
(my reverse proxy) based on running Docker containers. I already had a
way of doing that, but containuum is designed to be a bit more robust,
and I made it so the config can be passed over a network connection
instead of having to write it to disk and then signal the proxy to reload.&lt;/p&gt;
&lt;p&gt;The next project has a bit of backstory: after reading
&lt;a href=&#34;https://blog.jakesaunders.dev/my-server-started-mining-monero-this-morning/&#34;&gt;I got hacked, my server started mining Monero this morning&lt;/a&gt;
[warning: the article is interesting but a bit LLM-sloppy] I realised that I, too, was
running a vulnerable version of Umami. Luckily, as far as I can tell, it wasn’t
exploited. But it got me looking into automated vulnerability scanning. I don’t
put much stock into it normally, as you get so many false alerts, but this is one
case where it would have definitely helped. There didn’t seem to be any
good solution for my “middle-of-the-road” approach to containers (lots of containers,
but not needing Kubernetes), so I wrote &lt;a href=&#34;https://github.com/csmith/purser&#34;&gt;purser&lt;/a&gt;.
It takes every image used by a running container, runs them through
&lt;a href=&#34;https://github.com/aquasecurity/trivy/&#34;&gt;Trivy&lt;/a&gt;, and generates a combined report.
I’ll add some form of active notifications to it in the future.&lt;/p&gt;
&lt;h3 id=&#34;entertainment&#34;&gt;Entertainment&lt;/h3&gt;
&lt;p&gt;This month I’ve been playing a lot of Hogwarts: Legacy, after picking it up
for free on Epic Games. I grew up with the Harry Potter books coming out,
and while I’m not a super-fan, it still has a special appeal to me.
Obviously she-who-must-not-be-named is doing her best to out-evil Voldemort,
but I think you can still enjoy the universe despite that. Especially when
you don’t have to contribute any money that might go towards her.&lt;/p&gt;
&lt;p&gt;Anyway, the game is surprisingly fun. They did a really good job of making
it feel like an open world. You &lt;em&gt;want&lt;/em&gt; to walk around from place to place
to see everything, find all the various collectables, and so on. Compared
to Far Cry or Cyberpunk the world is a lot smaller but that means it’s far
more densely packed with stuff to do. I initially didn’t vibe with the
way combat works: it’s pretty heavily reliant on dodging or shielding against
incoming attacks, which reminded me a lot of Clair Obscur: Expedition 33.
Fortunately, it’s a lot more forgiving in Hogwarts: Legacy and I got used
to it after not too long.&lt;/p&gt;
&lt;p&gt;It’s been a relatively sedate month for other forms of entertainment.
I saw Fackham Hall and Wicked: For Good at the cinema. The former was
a funny romp, vaguely reminiscent of Airplane or The Naked Gun, as you’d
expect from something penned by Jimmy Carr. Wicked was… fine? I wasn’t
overly enamoured with part one, so it’s not a surprise I didn’t vibe with
the follow-up. If you smushed them into a single normal-length feature I think they
would have made a really good film, but two films both over two hours
long is a bit much for what actually happens.&lt;/p&gt;
&lt;h3 id=&#34;around-the-web&#34;&gt;Around the web&lt;/h3&gt;
&lt;h4 id=&#34;wikipedia-list-of-citogenesis-incidentshttpsenwikipediaorgwikiwikipedialist-of-citogenesis-incidents&#34;&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Wikipedia:List_of_citogenesis_incidents&#34;&gt;Wikipedia: List of citogenesis incidents&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A list of incidents where (often false) information on Wikipedia has ended up
becoming its own source. That led to Mark Dominus’ blog post titled
&lt;a href=&#34;https://blog.plover.com/wikipedia/ysolo.html&#34;&gt;Imaginary Albanian eggplant festivals… IN SPACE&lt;/a&gt;
where he discovers that a mountain on Ceres got named after a made-up festival
(and then got it fixed!)&lt;/p&gt;
&lt;p&gt;Not relying on Wikipedia as a source was drummed into us at school, but
it feels like this sort of thing is only going to get worse and worse now
LLMs are on the scene. I assume we’re already at the point where LLM
hallucinations are prevalent enough that they’ll find their way into
the training data for future iterations.&lt;/p&gt;
&lt;h4 id=&#34;simon-collisons-websitehttpscollycom&#34;&gt;&lt;a href=&#34;https://colly.com/&#34;&gt;Simon Collison’s website&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;I really like the design here. Both the homepage and how well every
journal entry is formatted. It makes me feel bad for the many walls of
text that adorn this site. The &lt;a href=&#34;https://colly.com/v4/&#34;&gt;archived v4 design&lt;/a&gt;
is also wonderful.&lt;/p&gt;
&lt;h4 id=&#34;my-dinner-with-skinnerhttpswwwyoutubecomwatchvpk-oq8iytva&#34;&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=pk-Oq8iYtVA&#34;&gt;My Dinner with Skinner&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A 43-minute-long feature film that is “just” the Steamed Hams gag from
The Simpsons done in the style of My Dinner with Andre. There’s a (much shorter)
&lt;a href=&#34;https://www.youtube.com/watch?v=1Ecg_5r1L1M&#34;&gt;behind-the-scenes video&lt;/a&gt; that
goes over the extreme amount of effort that was put into it, too.&lt;/p&gt;
&lt;p&gt;I’m still trying to wrap my head around this. It’s… something.&lt;/p&gt;
&lt;h4 id=&#34;delete-spotify-sure-but-dont-just-replace-it-with-another-subscriptionhttpsstephveecablogentertainmentdelete-spotify-sure-but-dont-just-replace-it-with-another-subscription&#34;&gt;&lt;a href=&#34;https://stephvee.ca/blog/entertainment/delete-spotify-sure-but-dont-just-replace-it-with-another-subscription/&#34;&gt;Delete Spotify? Sure, But Don’t Just Replace it With Another Subscription&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;A similar take to my own &lt;a href=&#34;https://chameth.com/escaping-spotify-the-hard-way/&#34;&gt;escaping Spotify the hard way&lt;/a&gt;,
but it avoids getting lost in the technical weeds like I did. (In my defence,
they’re very fun weeds to get lost in.) Stephanie is
also one of the people that does monthly wrap-ups, and I was pleasantly
surprised when &lt;a href=&#34;https://stephvee.ca/blog/rewinds/rewind-dec-2025/&#34;&gt;the latest one&lt;/a&gt;
popped up in my RSS reader and I saw my own site linked.&lt;/p&gt;
</content>
    </entry>
    <entry>
        <title>Finding an awkward bug with Claude Code</title>
        <link href="https://chameth.com/finding-an-awkward-bug-with-claude-code/"/>
        <updated>2025-06-30T00:00:00Z</updated>
        <id>https://chameth.com/finding-an-awkward-bug-with-claude-code/</id>
        <content xml:lang="en" type="html">&lt;figure class=&#34;image right&#34;&gt;
  &lt;picture&gt;
      &lt;source srcset=&#34;https://chameth.com/finding-an-awkward-bug-with-claude-code/confidence.avif&#34; type=&#34;image/avif&#34;/&gt;
      &lt;source srcset=&#34;https://chameth.com/finding-an-awkward-bug-with-claude-code/confidence.webp&#34; type=&#34;image/webp&#34;/&gt;
      &lt;img src=&#34;https://chameth.com/finding-an-awkward-bug-with-claude-code/confidence.png&#34; alt=&#34;Composite screenshot of 11 different Claude responses that are all very confident at having found the bug&#34; loading=&#34;lazy&#34; width=&#34;411&#34; height=&#34;264&#34;/&gt;
  &lt;/picture&gt;
  &lt;figcaption&gt;&lt;p&gt;The many times Claude thought it was right…&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I recently encountered a bug in one of my projects that I couldn’t immediately
figure out. It was an issue in &lt;a href=&#34;https://github.com/csmith/centauri&#34;&gt;Centauri&lt;/a&gt;,
my reverse proxy. After its config was updated, I noticed&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:1&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; it stopped serving
responses. Looking at the logs, I could see it was obtaining new certificates
from Let’s Encrypt for a couple of domains, but I’d designed it so that wouldn’t
block requests (or so I thought). After a few minutes of confusion, everything
went back to working.&lt;/p&gt;
&lt;p&gt;It felt like an issue with locking, but a quick glance didn’t show me anything
obvious. I decided to throw the problem at Claude Code and see how it did.
With some guidance&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:2&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt; it managed to identify the cause, but it was a bit
of a journey to get there. I’m going to share the prompts and responses,
as it was an interesting process.&lt;/p&gt;
&lt;h3 id=&#34;the-initial-prompt&#34;&gt;The initial prompt&lt;/h3&gt;
&lt;p&gt;I thought the issue might be quite obvious, and I’d just overlooked it.
Centauri isn’t that complex a project, after all. So I gave Claude a fairly
minimal prompt to set it off:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&amp;gt; Hello! Please examine this codebase. I have noticed an issue that when a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  certificate is being renewed, the application seems to not serve any other
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  requests. I suspect there is a misplaced lock somewhere.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  Can you see what&amp;#39;s happening? Think very hard.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Claude Code has &lt;a href=&#34;https://www.anthropic.com/engineering/claude-code-best-practices&#34;&gt;special hardcoded triggers&lt;/a&gt;
to engage “thinking” mode, which makes it take more time to think about
alternatives. The keywords are: “think”, “think hard”, “think harder”, and
“ultrathink”. I managed to not quite get the right incantation here, and only
enabled the lowest level&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:3&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;!--more--&gt;
&lt;p&gt;I’ll omit all the responses where Claude is “thinking” (talking to itself),
or decides to make todo lists then cross items off them then add different
items, and so on. Its responses get fairly lengthy; I won’t judge you too
harshly if you skim over them. Its first response was blessedly brief:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● I found the issue! Looking at the certificate manager code, I can see the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  problem:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;… and then it went back to thinking and reading files, because it had neither
found the issue nor could it see the problem. Ho-hum. Some time later it came
back with:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● I found the issue! The problem is in certificate/manager.go:53-54:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  m.store.LockCertificate(subject, altNames)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  defer m.store.UnlockCertificate(subject, altNames)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The GetCertificate method acquires a lock at the beginning and holds it for the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  entire duration of the certificate renewal process. This includes:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. Contacting the ACME server
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. Solving DNS-01 challenges (can take minutes for DNS propagation)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. Obtaining the certificate
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  4. Getting OCSP staple
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  5. Saving to disk
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  During this time, any other request trying to get the same certificate will be
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  blocked. While the lock is per-certificate (not global), it still means that all
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  requests for that specific domain are blocked during renewal.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  Let me check if there are other places where this might cause blocking:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And it then proceeded to do even more thinking, checking files, etc, until
it concluded with:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● I found the root cause! The issue is actually worse than just a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  per-certificate lock. Here&amp;#39;s what&amp;#39;s happening:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The main blocking issue is in proxy/manager.go:133-144:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  func (m *Manager) CheckCertificates() {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      m.lock.RLock()  // ← Acquires read lock on the entire manager
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      for i := range m.routes {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          route := m.routes[i]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          // ...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          m.updateCert(route)  // ← This can take MINUTES for ACME renewal!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      m.lock.RUnlock()  // ← Only released after ALL routes are processed
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The blocking cascade:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   1. CheckCertificates holds a read lock on the entire proxy manager while
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      renewing ALL certificates
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   2. Certificate renewal via ACME can take several minutes per certificate
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      (DNS propagation, etc.)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   3. Every incoming request calls routeFor (proxy/manager.go:122) which needs
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      the same lock:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    func (m *Manager) routeFor(domain string) *Route {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        m.lock.RLock()  // ← BLOCKED until CheckCertificates completes!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        match := m.domains[strings.ToLower(domain)]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        m.lock.RUnlock()
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        // ...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  So when certificate renewal starts, ALL incoming requests are blocked because
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  they can&amp;#39;t even look up which route to use. This explains why the application
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  doesn&amp;#39;t serve any requests during renewal - they&amp;#39;re all waiting for the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  manager&amp;#39;s read lock.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The issue is compounded by:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - Certificate renewal happens every 12 hours automatically
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - It also happens when routes are updated (SIGHUP)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - Each certificate renewal involves time-consuming ACME operations
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  This is a critical performance bug where I/O operations (certificate renewal)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  are performed while holding a critical lock.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This doesn’t actually follow on from the earlier response, it’s completely
flipped onto a different lock being the issue. Amusingly, it’s nearly right
here, but it’s highlighting this code for completely the wrong reason.&lt;/p&gt;
&lt;p&gt;The lock it’s talking about is a &lt;code&gt;RWMutex&lt;/code&gt;, which is specifically designed to
allow many read-locks. Everything it’s pointing out are read locks, so they
cannot block one another as it described. The response Claude has given sounds
very confident, seems appropriately technical, but is in fact complete rubbish.
This is one of the big issue with all LLMs at the minute, and it makes me
despair at the idea of people “vibe coding” things when they don’t know enough
to push back against nonsense like this.&lt;/p&gt;
&lt;h3 id=&#34;course-correction&#34;&gt;Course correction&lt;/h3&gt;
&lt;p&gt;I explained briefly to Claude how this basic concurrency primitive works,
and it came right back with another solution:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&amp;gt; But that&amp;#39;s a RW Mutex. You can hold many read locks concurrently.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● You&amp;#39;re absolutely right! I apologize for the confusion. With a RWMutex,
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  multiple readers can hold the lock concurrently. Let me trace through this
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  more carefully.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Task(Analyze updateCert method)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  ⎿ Done (8 tool uses · 20.0k tokens · 1m 11.9s)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Excellent analysis! You&amp;#39;re right about RWMutex allowing concurrent readers.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The real issue is the race condition in the certificate store&amp;#39;s lock
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  management.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  Looking at certificate/store.go:82-91:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  func (j *JsonStore) lockFor(subjectName string, altNames []string) *sync.Mutex {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      key := strings.Join(append([]string{subjectName}, altNames...), &amp;#34;;&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      if mu, ok := j.locks[key]; ok {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          return mu
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          mu = &amp;amp;sync.Mutex{}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          j.locks[key] = mu  // ← Concurrent map write!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          return mu
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  This is a classic concurrent map access bug. When multiple requests hit the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  same domain during certificate operations:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. Multiple goroutines call lockFor() simultaneously
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. They read/write to j.locks map without synchronization
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. Go runtime can:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Panic with &amp;#34;concurrent map writes&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Corrupt the map structure
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Block indefinitely if map internals get corrupted
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  This explains the blocking behavior - it&amp;#39;s not the RWMutex, but rather a race
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  condition causing either panics or deadlocks when the map gets corrupted
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  during concurrent certificate operations.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The fix would be to either:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. Add a mutex to protect j.locks map access
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. Use sync.Map instead of a regular map
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. Pre-initialize all locks at startup
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I do appreciate when me stating a basic fact is called out as “excellent
analysis”. Definitely doesn’t make me feel like I’m having smoke blown up my
ass.&lt;/p&gt;
&lt;p&gt;I think the issue it’s describing here is actually a problem&lt;sup id=&#34;fnref:4&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:4&#34; role=&#34;doc-noteref&#34;&gt;4&lt;/a&gt;&lt;/sup&gt;, albeit one
that’s unlikely to be hit. But it’s definitely not the one we’re looking
for. Claude even describes the possible consequences of the bug, and they
don’t match what happened at all.&lt;/p&gt;
&lt;h3 id=&#34;maybe-logs-will-help&#34;&gt;Maybe logs will help?&lt;/h3&gt;
&lt;p&gt;While Claude was chugging away, I’d been looking into the issue as well. I
had logs from when it happened, and figured Claude may be able to glean
something from them too. The first thing I’d noticed is that I wasn’t logging
when a process finished, so I’d already added that to the code base, hence
the note so Claude doesn’t get itself confused:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&amp;gt; That may be an issue, but it&amp;#39;s not the one I was looking for. I&amp;#39;ve found logs
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  from the time it happened. Note that a few logs have been added to the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  codebase since (so we can&amp;#39;t tell from the logs when ACME or OCSP processes
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  *finished*, just when they started). Here are the logs:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:40:53.187887976Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Received signal, updating routes...&amp;#34;,&amp;#34;signal&amp;#34;:1}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:40:53.19947532Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:40:53.199507942Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:09.048465968Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:09.048482309Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:17.419299795Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:17.419325744Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:23.238027386Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Received signal, updating routes...&amp;#34;,&amp;#34;signal&amp;#34;:1}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:29.994330646Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:29.994353329Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:43.658794811Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:43.658819548Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:57.635325513Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:41:57.635347515Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:10.52439779Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:10.524417107Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:23.159430249Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:23.159452171Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:36.347407326Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:36.347433295Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:52.544628775Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:42:52.544653993Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:43:25.409579107Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:43:25.409602842Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:43:43.819479448Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:43:43.819502041Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:43:54.358957563Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:43:54.358979355Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:44:06.180278128Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Renewing certificate&amp;#34;, &amp;lt;big list of alt names&amp;gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:44:06.180302585Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;INFO&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Starting ACME process to obtain certificate&amp;#34;, &amp;lt;big list of alt names&amp;gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:45:03.830331922Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;WARN&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Failed to connect to upstream&amp;#34;,&amp;#34;error&amp;#34;:&amp;#34;context canceled&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:45:03.830528134Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;WARN&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Failed to connect to upstream&amp;#34;,&amp;#34;error&amp;#34;:&amp;#34;context canceled&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:45:03.832388931Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;WARN&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Failed to connect to upstream&amp;#34;,&amp;#34;error&amp;#34;:&amp;#34;context canceled&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:46:10.225854865Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;WARN&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Failed to connect to upstream&amp;#34;,&amp;#34;error&amp;#34;:&amp;#34;context canceled&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:46:10.488432434Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;WARN&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Failed to connect to upstream&amp;#34;,&amp;#34;error&amp;#34;:&amp;#34;context canceled&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:46:10.488439397Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;WARN&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Failed to connect to upstream&amp;#34;,&amp;#34;error&amp;#34;:&amp;#34;context canceled&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  {&amp;#34;time&amp;#34;:&amp;#34;22:46:30.617614536Z&amp;#34;,&amp;#34;level&amp;#34;:&amp;#34;WARN&amp;#34;,&amp;#34;msg&amp;#34;:&amp;#34;Failed to connect to upstream&amp;#34;,&amp;#34;error&amp;#34;:&amp;#34;context canceled&amp;#34;, ...}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I’ve cut down the logs a bit to remove the specific domains, simplify the
timestamps, and remove a bunch of other cruft. The key points are there were
two SIGHUPs received, and the last certificate before everything started breaking
covered 6 different hostnames.&lt;/p&gt;
&lt;p&gt;Claude then went through a phase of periodically shouting “Eureka!” then
deciding it hadn’t actually found anything yet:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Now I see it! The key insight is in the timing and the &amp;#34;context canceled&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  errors. Let me trace through what&amp;#39;s happening:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Ah, now I see what&amp;#39;s happening! Looking at the logs more carefully:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. 22:40:53 - First SIGHUP received, routes updated
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. 22:40:53 to 22:44:06 - Multiple certificates start renewing (14 certificates!)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. 22:41:23 - Another SIGHUP received while renewals are still in progress
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  4. 22:45:03 onwards - &amp;#34;context canceled&amp;#34; errors start appearing
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The issue is that SetRoutes is being called AGAIN (at 22:41:23) while the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  previous CheckCertificates() goroutine is still running and renewing
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  certificates. Let me check what happens in this scenario:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&amp;gt; (The context canceled errors are clients timing out because centauri wasn&amp;#39;t
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  proxying properly)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Ah, now I understand! The &amp;#34;context canceled&amp;#34; errors are from clients timing
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  out because Centauri wasn&amp;#39;t proxying their requests. This confirms the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  blocking issue.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  Let me check how incoming requests actually get their certificates:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Now I found the real issue! Looking at the timeline and the code:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. 22:40:53 - SIGHUP triggers SetRoutes() which:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Calls loadCertificate() for each route
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Sets route.certificate to the existing certificate
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Starts go m.CheckCertificates() in background
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. 22:41:23 - Another SIGHUP while certificates are still renewing
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - SetRoutes() is called AGAIN
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Creates NEW Route objects with route.certificate = nil initially
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Replaces the old routes in m.routes
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - The old Route objects (with valid certificates) are replaced!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The critical issue is in proxy/manager.go:42-65:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  func (m *Manager) SetRoutes(newRoutes []*Route, fallback *Route) error {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      // ... validation ...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      for i := range newRoutes {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          route := newRoutes[i]  // These are NEW Route objects!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          // ...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;          m.loadCertificate(route)  // Try to load certificate
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      m.lock.Lock()
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      m.routes = newRoutes  // REPLACES all routes!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      m.lock.Unlock()
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  When the second SIGHUP arrives:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. New Route objects are created
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. For domains currently being renewed, GetExistingCertificate might fail or
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     return expired certs
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. These routes get route.certificate = nil and CertificateMissing status
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  4. CertificateForClient returns nil for these domains
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  5. TLS handshake fails → clients can&amp;#39;t connect → &amp;#34;context canceled&amp;#34; errors
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The fix would be to either:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. Prevent concurrent CheckCertificates runs
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. Copy certificates from existing routes before replacing them
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. Make certificate renewal truly independent of route management
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Midway through it had started searching for “context canceled” errors in the
codebase, which doesn’t make much sense. That’s what happens when a client gives
up waiting, so the corresponding backend request is terminated. You won’t
actually find “context canceled” anywhere in Centauri, because it’s a feature
of the Go standard library. I gave it a little nudge to try to stop it rabbit
holing.&lt;/p&gt;
&lt;p&gt;As far as I can tell, what it’s talking about in this response isn’t an issue.
I guess it’s possible that there’s a race condition whereby a route would be
replaced and end up with an old version of a certificate that was just being
renewed, but it wouldn’t cause an issue in practice; it’d just get updated
next time the certificates were refreshed.&lt;/p&gt;
&lt;p&gt;There’s no way for routes to get a &lt;code&gt;nil&lt;/code&gt; certificate in this scenario, as far
as I know. But I don’t have the unfounded confidence of an LLM, so I simply
said:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&amp;gt; I don&amp;#39;t follow why the certificate would be nil?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;attempt-the-third&#34;&gt;Attempt the third&lt;/h3&gt;
&lt;p&gt;I expected Claude to just explain its reasoning, but it apparently didn’t have
any reasoning so set off on another voyage of discovery:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● You&amp;#39;re right to question that. Let me trace through this more carefully.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Good point! Since GetExistingCertificate doesn&amp;#39;t acquire locks, it should
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  return the existing certificate even during renewal. Let me dig deeper:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● Now I see a potential race condition! Here&amp;#39;s what could happen:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● I found it! There&amp;#39;s a critical race condition that explains the blocking:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The Race Condition
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. Background renewal starts: CheckCertificates() → updateCert() →
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     GetCertificate() acquires lock and starts ACME renewal
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. SIGHUP arrives: SetRoutes() is called, which for each route calls
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     loadCertificate() → GetExistingCertificate() → store.GetCertificate()
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. The race:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - store.GetCertificate() returns a pointer to the certificate in the slice
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Meanwhile, the renewal completes and calls SaveCertificate()
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - SaveCertificate() calls removeCertificate() which modifies the slice while
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      it&amp;#39;s being read
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - This slice modification (append(j.certificates[:i], j.certificates[i+1:]...))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;      can cause:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        - The pointer returned by GetCertificate to become invalid
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        - Slice bounds issues
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;        - Memory corruption
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  4. The result:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - GetExistingCertificate might return corrupted data or nil
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Routes end up with certificate = nil
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - TLS handshakes fail
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;    - Clients timeout → &amp;#34;context canceled&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The Timeline Confirms This
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - 22:40:53 - First SIGHUP, starts renewal of 14 certificates
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - 22:41:23 - Second SIGHUP arrives (30 seconds later) while renewals are still running
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - 22:45:03 - First timeout errors (4 minutes after first SIGHUP)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The 4-minute delay matches ACME renewal time. The second SIGHUP&amp;#39;s SetRoutes
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  likely read certificates while they were being modified by the first SIGHUP&amp;#39;s
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  renewal goroutines.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  This is a classic concurrent slice modification bug - no synchronization
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  between readers (GetCertificate) and writers (SaveCertificate/removeCertificate).
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Once again we have a very confident and technical answer that does not explain
the symptoms we’re looking for. It’s also mostly rubbish again.
&lt;code&gt;GetCertificate&lt;/code&gt; and &lt;code&gt;SaveCertificate&lt;/code&gt; are only called by code that is holding
a lock for that certificate; they won’t interfere with one another. There may,
again, be a small concurrency issue if different certificates are handled in
parallel, but it’s definitely not the cause of our current problems.&lt;sup id=&#34;fnref:5&#34;&gt;&lt;a class=&#34;footnote-ref&#34; href=&#34;#fn:5&#34; role=&#34;doc-noteref&#34;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Working through these explanations and debunking them was annoying me at this
point. I decided to give it once last chance, and used the “ultrathink”
shibboleth:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&amp;gt; So there may be a concurrent modification issue, but that doesn&amp;#39;t explain the
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  symptoms. If a TLS handshake failed it wouldn&amp;#39;t result in the clients timing
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  out. Likewise if there was any kind of corruption there&amp;#39;d be a panic, not a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  period where it didn&amp;#39;t work and then started working again. Engage ultrathink.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;fourth-time-lucky&#34;&gt;Fourth time lucky&lt;/h3&gt;
&lt;p&gt;I’m going to include a piece of the thinking here because it amused me so much:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;✻ Thinking…
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  Oh! I need to think about this differently. Let me trace through what happens
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  when a request comes in:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Apparently it takes “ultrathink” to actually work through the problem logically?
Amusingly it did actually get there:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● You&amp;#39;re absolutely right. Let me think more carefully about this...
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;● EXACTLY! That&amp;#39;s the issue. Looking at the timeline:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  1. 22:40:53 - First SIGHUP: CheckCertificates() acquires READ lock, starts
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     renewing 14 certificates
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  2. 22:41:23 - Second SIGHUP: SetRoutes() tries to acquire write lock, blocks
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     waiting
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  3. Incoming requests call routeFor() for route lookup, but Go&amp;#39;s RWMutex blocks
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     new readers when a writer is waiting (to prevent writer starvation)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  4. 22:45:03 - Clients timeout because requests can&amp;#39;t even get past route lookup
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The key insight is Go&amp;#39;s RWMutex writer starvation prevention: when a writer
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  is waiting, new readers are blocked even though the current lock holder is
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  also a reader.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  So the cascade is:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - CheckCertificates (read lock) → certificate renewal takes minutes
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - SetRoutes (write lock) → waits for read lock to release
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - routeFor (read lock for every request) → blocked by waiting writer
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  Result: All incoming HTTP requests hang at route lookup, never reach TLS
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  handshake or proxying logic. Clients timeout waiting for any response
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  → &amp;#34;context canceled&amp;#34;.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  This perfectly explains:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - No panics (no corruption)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - No immediate TLS errors (never gets that far)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - Requests hang then timeout (blocked on lock)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  - Eventually recovers (when certificate renewals finish)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  The fix would be restructuring to avoid holding the manager lock during slow
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;  certificate operations.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We’re back to the &lt;code&gt;RWMutex&lt;/code&gt; again, but this time it’s dropping knowledge on me
not the other way around: if there’s a writer waiting, then new readers are
blocked. This does indeed perfectly explain the symptoms, and was something
I’d overlooked. The write lock is only held briefly to update some struct
members to prevent it from blocking the readers, but this behaviour coupled
with the long-running read lock ends up doing that anyway.&lt;/p&gt;
&lt;h3 id=&#34;the-fix&#34;&gt;The fix&lt;/h3&gt;
&lt;p&gt;After all this work it’s very tempting just to ask Claude to fix it, but I
had almost no confidence it’d be able to maintain any of the concurrency
requirements given how much it flailed around with bad explanations. But in the
interests of science, I asked it to try. Here’s what it came up with:&lt;/p&gt;
&lt;pre class=&#34;chroma-chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   // CheckCertificates checks and updates the certificates required for registered routes.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   // It should be called periodically to renew certificates and obtain new OCSP staples.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   func (m *Manager) CheckCertificates() {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+    // Create a snapshot of routes to avoid holding the lock during slow certificate operations
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     m.lock.RLock()
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+    routes := make([]*Route, len(m.routes))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+    copy(routes, m.routes)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+    provider := m.provider
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+    m.lock.RUnlock()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+    // Process certificate renewals without holding the manager lock
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gd&#34;&gt;-    for i := range m.routes {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+    for i := range routes {
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gd&#34;&gt;-      route := m.routes[i]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gi&#34;&gt;+      route := routes[i]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;       if provider == nil {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;         route.certificateStatus = CertificateNotRequired
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;       } else {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;         m.updateCert(route)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;       }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;     }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;&lt;span class=&#34;chroma-gd&#34;&gt;-    m.lock.RUnlock()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;chroma-line&#34;&gt;&lt;span class=&#34;chroma-cl&#34;&gt;   // updateCert updates the certificate for the given route.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That’s about what I expect. It definitely fixes the exact issue described, but
it’s a bit… sloppy. Routes are now being updated when the lock isn’t being held,
which is a change in semantics. It also doesn’t address the underlying issue
that requests can be blocked if there’s a pending writer, and pending writes
can be blocked by readers. A better approach would be to allow these operations
to happen without blocking, maybe by using an atomic pointer to an immutable
slice. I guess I’ve got some coding to do!&lt;/p&gt;
&lt;p&gt;As for Claude: while the journey was frustrating, I think trying to figure this
out by myself would have been even more so. It’s one of those things where
everything &lt;em&gt;looks&lt;/em&gt; right to me, and I’d have to go down a fairly long chain
of checking my assumptions to find where I’d gone wrong.&lt;/p&gt;
&lt;p&gt;There’s definitely a lot of room for improvement in coding LLMs: being able
to estimate their own confidence level instead of stating everything as
absolute truth would go a long way. I imagine there’s also a lot of benefit
of having one LLM vet the responses of another; that’s sort-of how thinking
mode works, but it doesn’t seem to go far enough to stop it outputting rubbish.
It’ll be interesting to see how they develop in the future, anyway.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr/&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;read as “I got spammed by a bunch of monitoring notifications” &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:1&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:2&#34;&gt;
&lt;p&gt;read as “Me repeatedly telling it that it was talking rubbish” &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:2&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:3&#34;&gt;
&lt;p&gt;If only we had some kind of language model technology that could understand this kind of thing. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:3&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:4&#34;&gt;
&lt;p&gt;I sense some fun times with Go’s race detector in my near future &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:4&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&#34;fn:5&#34;&gt;
&lt;p&gt;The most likely outcome from these concurrency issues is Centauri panicking,
which isn’t great but would only interrupt ongoing connections before it
restarts and starts serving traffic again. Much less problematic than the
“block for five minutes” behaviour I’m trying to fix. &lt;a class=&#34;footnote-backref&#34; href=&#34;#fnref:5&#34; role=&#34;doc-backlink&#34;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
</content>
    </entry>
</feed>
