bdsh: A dsh-alike for me 01 Feb 2026

I finally put enough of a bow-tie on bdsh that I can use it as a daily tool. WOO HOO! I have long wanted a dsh which did a few additional things, and now I have them. In honor of dsh's backronym ambiguity, I am not sure if bdsh is "better (distributed|dancer's) shell", "Brian's (distributed|dancer's) shell", or "BreakDancer's Shell". Regardless, I am super happy with this little tool :-) So, what does it do differently?

First, it has the idea of a consensus view for the outputs frmo each host. When running it builds and displays a consensus view of the most common line-by-line output, with drill down into where it differs. This is probably best illustrated with a demo:

cowsay demo

This demo shows running cowsay across three hosts, rendering the unified output view, and drilling down into a difference.

Second, notice it is running in tmux. This lets you switch over to the windows and see them, and critically if something there requires interaction, you can interacta common thing I use it for is bdsh :bsd -- sudo "sh -c 'freebsd-update fetch && freebsd-update install; pkg update && pkg upgrade'" which wants input on updates. You can circumvent this, but FreeBSD makes you feel guilty when you do.. If you are doing this across hundreds of hosts, not so great, but I am ususally doing it across a reasonable number, so is actually useful. I also added a heuristic to detect if it thinks a given host is waiting on input, and changing the status indactor to a little blinking keyboard:

input demo

Window 0 in tmux is the unified output rendering process. When it thinks input is desired it does the little kwyboard icon and tells you the window number, so you can jump to it quickly. That window 0 process will exit on q but the actual invocations will continue in their windows, exiting their window when they finish, and cleaning up tmux.

Finally, in these examples I list the hosts to execute on, but it can pull from a hosts file at ~/.config/bdsh/hosts, from a shell command, or script, etc. You can read about it. The format of output here is just line delimited hostnames with optional tags, in the form:

hati.brianm.dev         :bsd    :cloud
h0001.brianm.dev        :arch   :cloud
h0002.brianm.dev        :arch   :cloud

pancake.home            :bsd    :home
freki.home              :bsd    :home
badb.home               :bsd    :home
v0003.home              :arch   :home
v0004.home              :arch   :home

m0001.home              :bsd    :home
m0002.home              :bsd    :home

m0003.barn              :bsd    :barn
m0004.barn              :bsd    :barn

Where you can filter on tags, like: bdsh :bsd:home -- do_stuff which ANDs or bdsh :barn,:cloud -- do_stuff which ORs. Of course, you can combine them as well, with AND binding tighter. With no filter, everything is selected.

Currently bdsh is packaged for homebrew (brew install brianm/tools/bdsh) and arch (bdsh in AUR), but I'll probably publish a deb soon, and once I think it is stable, and if there is evidence of anyone besides myself using it, see if I can get a port/pkg merged for FreeBSD. Source, basic docs, and more docs all on github, Apache-2.0 licensed.


D&D (prep) with Claude Code 11 Jan 2026

I have teenage kids, and they are possibly dorkier than me, which is awesome. Since the pandemic, we have had a D&D game going, off and on, with some of their friends and friends' parents. It's been super fun. I am, however, the perma-DM. I am 99% fine with this, as I love the world building aspects which the DM gets to lean more heavily into than players.

Sometime this past year I started doing my session prep in Claude Code. It started with discussing ideas for arcs and settings, and rapidly turned into shockingly useful session prep. While I am impressed by frontier models' ability to program, I am in awe of their ability to help plan and prep D&D because this is very much about predicting human behavior, setting up interesting situations and being ready for what real people may do in them. That Claude can make rustc sing is nice, that it can make my players sing was unexpected.

I have, for a long time, been keeping my notes in Obsidian and git (private GH repo), laid out more or less like:

├── encounters
│   ├── agadol_ambush.md
│   ├── Audience with Kerral.md
│   └── [more, elided ...]
├── factions
│   ├── Arjun.md
│   ├── Tollkeepers.md
│   └── [more elided ...]
├── ideas
│   ├── beechport_arc_ideas.md
│   ├── The Red Dragon.md
│   └── [more, elided ...]
├── npc
│   ├── Agustin.md
│   ├── Chalan.md
│   └── [and more, elided...]
├── pc
│   ├── Borin - Notes.md
│   ├── Ethex - Notes.md
│   ├── Nameless - Notes.md
│   └── Ragux - Notes.md
├── places
│   ├── Aurum.md
│   ├── Beechport.md
│   └── [and more, elided ...]
└── sessions
    ├── [and more elided ...]
    ├── Session 026 - 2026-01-03.md
    └── Session 027 - 2026-01-17.md

Claude took to this structure like, well, a coding agent to a tree of markdown :-) I have long described myself as working best when I can "pair think" -- put me at a whiteboard with another programmer and I, at least, am more than 2x as effective. Cannot vouch for any given pair-thinker, but this seems to be common enough that I suspect the sum is greater than the parts. Rubber ducking, even with Jonathan Aquino's excellent assistance, Thank you for that video, Jon, I still use it! just is not the same. Turns out DM planning works this way to, and Claude is a good enough pair-thinker that the results are great.

Mechanically, I fire up a claude-code web session to plan the next session and just let thoughts wander all over. I let claude organize what we come up with into NPC, Faction Idea, or Session files as it makes sense, then iterate on key things for the next session. I generally have a bunch of "here is other stuff going on" in a per-arc doc (beechport_arc_ideas.md above, for example), which act as a catch all for "this might happen, and I think here are some things that might fall out of it..." type thoughts. The arc-doc is the big picture context for the next session of planning, and it will refer to key NPCs, places, factions, etc.

Historically I used something similar to the Lazy DM approach, but have been trying Brennan Lee Mulligan's toy approach lately. In both cases, Claude has been able to take a description of the materials I want at the table and produce them well enough to almost just use.

As I mentioned, the thing that has most impressed me is in encounter planning. Claude is surprisingly good at predicting what players will do in given situations, estimating the time for a given encounter, estimating the difficulty, highlighting things which will likely have an emotional impact on a specific player, etc. It can predict good beats to highlight aspects of individual characters even, and help set opportunities for specific individuals to showcase some aspect of themselves. It sometimes gets confused between NPCs and PCs, and will optimize to allow an NPC to shine, but it course corrects on this well enough.

The main thing that burnt me early was when Claude would go off the rails Is it still hallucination if the thing it is hallucinating is completely imaginary and poorly defined in the first place? on aspects of the world or players and I didn't bother to correct it, because, who cares. Later when it referred to past notes the misunderstandings started magnifying and the overall quality as a planning partner deteriorated greatly. Spending time getting on the same page, and getting Claude to record it in the various docs, was well worth it, as now it makes really good connections without help.


Fun With HTTP Caching 27 Dec 2025

The two fun problems in computer science: cache invalidation, naming things, and off-by-one errors. Today I want to talk about the first one.

I've been working on Epithet, an SSH certificate authority Really, an agent, a CA, and policy server that makes certificate-based authentication easy. Part of the system involves a "discovery" endpoint where clients learn which hosts the CA handles. The question: how do you cache this efficiently while still allowing updates to propagate?

The policy server component provides a discovery endpoint for this, which serves a simple JSON document:

{"matchPatterns": ["*.example.com", "prod-*"]}

This content rarely changes, only when you add a new host pattern to your policy, generally. But when it does change, you want clients to pick it up reasonably quickly. My first implementation used aggressive caching:

w.Header().Set("Cache-Control", "max-age=31536000, immutable")

Cache it forever! The URL is content-addressed (/d/{hash}), so if the content changes, the hash changes, and you get a new URL. Problem solved, right?

Not quite. The client learns the discovery URL from a Link header in other responses:

Link: </d/abc123>; rel="discovery"

But the client caches this URL. If the server starts returning a different URL in the Link header, the client won't notice until... when exactly?

The discovery document at /d/abc123 is immutable and cached forever. The client has no reason to re-fetch it. And it has no reason to make other requests that would reveal the new Link header. We've created an immortal cache entry.

The obvious fix: use ETag and If-None-Match for cache revalidation.

w.Header().Set("Cache-Control", "max-age=300")  // 5 minutes
w.Header().Set("ETag", `"` + hash + `"`)

if r.Header.Get("If-None-Match") == expectedETag {
    w.WriteHeader(http.StatusNotModified)
    return
}

This works: after 5 minutes, the client revalidates. If the content hasn't changed, it gets a quick 304. If it has, it gets the new content.

But there's a wrinkle. I want to support deploying discovery documents to a CDN or static file server (probably S3 fronted by a CDN, to be honest). If we rely on each of these URLs sending a 404 or a redirect then we need to either update every URL ever published when we make a change (to redirect to the new location), maintain a very long chain of redirects, or rely on out of band behavior if they start 404'ing (know to go fetch something which will include the new Link header). YUCK.

So, a layer of indirection solves everything, right? What if we separate "what's the current discovery document?" from "what's in that document?"

  • A pointer (/d/current) that says "the current discovery is at /d/abc123"
  • The content (/d/abc123) which is truly immutable

The pointer can have a short cache lifetime. The content can be cached forever. When the content changes:

  1. Deploy new content at /d/xyz789
  2. Update the pointer to redirect to /d/xyz789
  3. Clients' cached pointers expire after 5 minutes
  4. They fetch the pointer, get redirected to the new content
  5. Old content at /d/abc123 can stay around (or be garbage collected later)

The built-in policy server is in Go, so the redirect handler is trivial:

func NewDiscoveryRedirectHandler(hash string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Cache-Control", "max-age=300")
        w.Header().Set("Location", "/d/"+hash)
        w.WriteHeader(http.StatusFound)
    }
}

The content handler remains unchanged with immutable caching:

w.Header().Set("Cache-Control", "max-age=31536000, immutable")

The Link header now always points to /d/current:

w.Header().Set("Link", "</d/current>; rel=\"discovery\"")

HTTP caches handle this beautifully:

  1. Client requests /d/current
  2. Gets 302 redirect to /d/abc123 with max-age=300
  3. Follows redirect, gets content with immutable
  4. Both responses are cached appropriately
  5. After 5 minutes, the redirect expires
  6. Next request fetches /d/current again
  7. Might get same redirect (cache hit on content) or new one

The content-addressed URLs can be served from anywhere: a CDN, S3, a static file server. They never need invalidation logic. The redirect endpoint is the only "dynamic" part, and it's just returning a Location header.

The final implementation is about 10 lines of code. Most of the work was figuring out the right design :-)

The best part: this doesn't mandate a specific implementation. The contract between client and server is just "respect HTTP caching headers."

A policy server could:

  • Use this same redirect pattern
  • Use ETag with conditional requests
  • Use a short max-age without immutable
  • Some other scheme entirely

As long as the server sets appropriate Cache-Control headers and the client respects them, it works. The client just uses a standard RFC 7234 compliant HTTP cache (in my case, github.com/gregjones/httpcache.

Sometimes the best solution is realizing HTTP already solved your problem decades ago.

Want to try Epithet? Check out the GitHub repo.