<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://deemkeen.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://deemkeen.github.io/" rel="alternate" type="text/html" /><updated>2026-03-18T16:22:07+00:00</updated><id>https://deemkeen.github.io/feed.xml</id><title type="html">deemlog</title><subtitle>how to automate and solve your problems, so you can have new ones</subtitle><author><name>deemkeen</name><email>dimgur@gmail.com</email></author><entry><title type="html">The Language Your AI Agent Secretly Wishes You’d Use</title><link href="https://deemkeen.github.io/2026/03/18/the-language-your-ai-agent-secretly-wishes-youd-use/" rel="alternate" type="text/html" title="The Language Your AI Agent Secretly Wishes You’d Use" /><published>2026-03-18T00:00:00+00:00</published><updated>2026-03-18T00:00:00+00:00</updated><id>https://deemkeen.github.io/2026/03/18/the-language-your-ai-agent-secretly-wishes-youd-use</id><content type="html" xml:base="https://deemkeen.github.io/2026/03/18/the-language-your-ai-agent-secretly-wishes-youd-use/"><![CDATA[<p>Every programming language claims to be productive. But in 2026, there’s a new benchmark that matters: <strong>how well does an LLM write code in your language?</strong></p>

<p>Not how well <em>you</em> write code. How well does Claude, GPT, or Gemini generate correct, idiomatic, production-ready code — on the first try, without you hand-holding every line?</p>

<p>I’ve spent the past year building software with heavy AI assistance across multiple stacks, and the differences are staggering. Some languages feel like pair programming with a senior engineer. Others feel like babysitting an intern who read three conflicting textbooks overnight.</p>

<p>I ranked the most popular languages by a single criterion: <strong>how reliably does an LLM produce code you’d actually ship?</strong> We’re starting at the bottom. The #1 pick might surprise you.</p>

<hr />

<h2 id="10-c--the-graveyard-of-good-intentions">#10: C++ — The Graveyard of Good Intentions</h2>

<p>Let’s start with the carnage.</p>

<p>C++ deserves the bottom spot not because it’s obscure, but because it’s <em>popular</em> and <em>terrible</em> for LLM-generated code at the same time. A dangerous combination.</p>

<p>The language is so vast that no human fully masters it. No LLM does either. C++11, 14, 17, 20, 23 — each standard added features but removed nothing. For every task, there are five to ten historically valid approaches stacked across different eras: smart pointers or raw pointers? <code class="language-plaintext highlighter-rouge">std::string</code> or <code class="language-plaintext highlighter-rouge">const char*</code>? Templates or <code class="language-plaintext highlighter-rouge">void*</code>? RAII or manual cleanup? Exceptions or error codes? <code class="language-plaintext highlighter-rouge">std::variant</code> or union?</p>

<p>LLMs generate C++ that jumps wildly between these epochs. Modern <code class="language-plaintext highlighter-rouge">auto</code> and range-based for-loops next to C-style casts and <code class="language-plaintext highlighter-rouge">malloc</code>. Syntactically correct. Stylistically schizophrenic. Hiding undefined behavior that works on your machine and segfaults in production.</p>

<p>Template metaprogramming makes it worse. Error messages that <em>humans</em> can barely read. An LLM trying to iteratively fix template errors enters a death spiral of increasingly creative but wrong solutions, each one spawning three new errors.</p>

<p>The cruel irony: C++ has probably the second-largest training corpus after Python. But quantity doesn’t help when the data itself is contradictory — and with C++, it is by definition, because the language permits five decades of paradigms to coexist in a single file.</p>

<p><strong>Verdict:</strong> Maximum training data. Maximum inconsistency. The language where AI-assisted development actively makes you <em>less</em> productive because you spend more time reviewing than you saved generating.</p>

<hr />

<h2 id="9-java-without-framework--all-dressed-up-nowhere-to-go">#9: Java (Without Framework) — All Dressed Up, Nowhere to Go</h2>

<p>Plain Java is a strange beast. It has strong typing, a mature compiler, and a gigantic training corpus. On paper, that’s a winning hand.</p>

<p>In practice, it’s a language nobody actually uses without a framework. And that’s the problem — without Spring Boot or Quarkus constraining the solution space, an LLM faces a combinatorial explosion. “Build a REST endpoint” could mean Spring MVC, Spring WebFlux, JAX-RS with Jersey, Quarkus RESTEasy, Micronaut — each with different annotations, different conventions, different implicit behaviors.</p>

<p>The verbosity compounds the issue. Entity, Repository, Service, Controller, DTO, Mapper — Java’s ceremony is extreme. An LLM burns through output tokens on boilerplate, and longer outputs mean more opportunities for the model to drift, hallucinate, or quietly start cutting corners with <code class="language-plaintext highlighter-rouge">// ... rest of implementation</code>.</p>

<p>Java without a framework is like a race car without a track. Powerful engine, no direction.</p>

<p><strong>Verdict:</strong> Strong fundamentals betrayed by ecosystem fragmentation and crippling verbosity.</p>

<hr />

<h2 id="8-c--beautiful-simplicity-zero-safety-net">#8: C — Beautiful Simplicity, Zero Safety Net</h2>

<p>Here’s a plot twist: C is <em>surprisingly</em> LLM-friendly — more so than most people expect.</p>

<p>The language is tiny. Really tiny. No overloading, no templates, no inheritance, no exceptions, no generics, no closures, no garbage collector. An LLM can master the <em>entire</em> language specification, not just the commonly-used subset. That’s a property it shares with only one other language on this list — and that language is #1.</p>

<p>Idiomatic C looks the same everywhere. A <code class="language-plaintext highlighter-rouge">struct</code> with functions that operate on it. Manual memory allocation and deallocation. Pointer passing for output parameters. Header files declaring public interfaces. The patterns haven’t changed in decades, which means the training data is extraordinarily consistent across eras. A well-written C function from 1995 looks almost identical to one written in 2025.</p>

<p>For straightforward systems code — parsers, protocol implementations, data structures, CLI tools — LLMs generate remarkably clean C. The absence of abstraction mechanisms means there’s nowhere to over-engineer. You can’t accidentally create a factory-builder-strategy-observer monstrosity in C because the language simply doesn’t have the vocabulary for it.</p>

<p>But there’s a dealbreaker that overrides all of this: <strong>memory safety doesn’t exist.</strong></p>

<p>Buffer overflows, use-after-free, double-free, off-by-one pointer arithmetic, null pointer dereferences, uninitialized reads, format string vulnerabilities — these are exactly the error classes where LLMs fail systematically, because the bugs are <em>syntactically invisible</em>. The code compiles cleanly. The tests pass. Valgrind might catch it, ASan might catch it — but the compiler won’t, and an LLM iterating on compiler output has no signal that anything is wrong.</p>

<p>This isn’t theoretical. Ask an LLM to implement a dynamic array in C — <code class="language-plaintext highlighter-rouge">realloc</code>, <code class="language-plaintext highlighter-rouge">memcpy</code>, size tracking. The generated code will look reasonable. It might even work for your test cases. But the edge cases — what happens when <code class="language-plaintext highlighter-rouge">realloc</code> returns a different pointer? When <code class="language-plaintext highlighter-rouge">size_t</code> wraps around on multiplication? When the caller frees the array and then accesses it through a stale pointer? — those are the bugs that slip through, because nothing in the language forces you to handle them.</p>

<p>In Go, an out-of-bounds access panics immediately. In Rust, it doesn’t compile. In C, it silently corrupts memory and you find out three months later in a crash dump — or worse, in a CVE report.</p>

<p><strong>Verdict:</strong> C is the most <em>underrated</em> language for LLM generation on the pure language design axis. Small, explicit, consistent, zero magic. But the complete absence of safety nets turns every AI-generated function into a trust exercise. The language that would rank top 3 if it had a bounds checker ranks #8 because it doesn’t.</p>

<hr />

<h2 id="7-c--the-quiet-all-rounder-nobody-talks-about">#7: C# — The Quiet All-Rounder Nobody Talks About</h2>

<p>C# does a lot of things right. Strong typing, good tooling, a rich standard library, and a reasonably consistent ecosystem centered around .NET. LLMs produce decent C# — it’s Microsoft’s primary language, well-represented in training data, and the conventions are fairly stable.</p>

<p>But it lives in a no-man’s-land. It’s not as minimal as Go, not as type-powerful as TypeScript, not as ecosystem-dominant as Java. The framework fragmentation is moderate — .NET has had its own share of historical pivots (ASP.NET → ASP.NET Core → Minimal APIs), and the DI/middleware patterns share some of the same implicit magic that haunts Spring Boot.</p>

<p><strong>Verdict:</strong> Solid, unremarkable, and consistently good enough. The language equivalent of a reliable sedan — you won’t write blog posts about it, but it gets you there.</p>

<hr />

<h2 id="6-kotlin--the-heir-that-arrived-too-late">#6: Kotlin — The Heir That Arrived Too Late</h2>

<p>Kotlin is arguably the <em>best-designed</em> language on this list for human developers. Null safety baked into the type system, data classes, coroutines, extension functions — it fixes almost everything wrong with Java.</p>

<p>For LLM-generated code, it has one critical weakness: training data volume. Kotlin’s corpus is a fraction of Java’s or Python’s. LLMs know Kotlin well enough to write it, but not well enough to consistently write <em>idiomatic</em> Kotlin. You get code that looks like Java translated to Kotlin syntax — which works, but misses the point.</p>

<p>The multiplatform story (KMP) and the Compose ecosystem also fragment the patterns an LLM needs to learn. Backend Kotlin, Android Kotlin, and Compose Multiplatform Kotlin are almost three different dialects.</p>

<p><strong>Verdict:</strong> Give it three more years of training data and it jumps to top 4. Right now, it’s a better language than its ranking suggests — the LLMs just haven’t caught up yet.</p>

<hr />

<h2 id="5-python--the-uncomfortable-truth">#5: Python — The Uncomfortable Truth</h2>

<p>Python is where most people <em>start</em> with AI-generated code. It’s the default language for ChatGPT demos, the lingua franca of data science, and it has the largest training corpus of any language, period.</p>

<p>And yet it’s only #5. Here’s why.</p>

<p>No type enforcement at runtime. Type hints exist but are optional and unenforced. An LLM can generate a function that accepts a <code class="language-plaintext highlighter-rouge">str</code>, gets passed an <code class="language-plaintext highlighter-rouge">int</code>, and the error surfaces three layers deep in a completely unrelated traceback. The feedback loop is terrible.</p>

<p>The “many ways to do it” philosophy is the anti-Go. List comprehension or for loop? <code class="language-plaintext highlighter-rouge">dataclass</code> or <code class="language-plaintext highlighter-rouge">NamedTuple</code> or plain dict? <code class="language-plaintext highlighter-rouge">requests</code> or <code class="language-plaintext highlighter-rouge">httpx</code> or <code class="language-plaintext highlighter-rouge">urllib3</code> or <code class="language-plaintext highlighter-rouge">aiohttp</code>? <code class="language-plaintext highlighter-rouge">asyncio</code> or threading or multiprocessing? Every choice is valid, and LLMs mix styles freely within the same file.</p>

<p>Python’s strength for AI code generation is speed of iteration — you get <em>something</em> running fast. But “running” and “correct” are different things, and Python makes it very easy to produce code that’s the first but not the second.</p>

<p><strong>Verdict:</strong> Fastest time-to-working-prototype. But you’re trading compile-time safety for runtime surprises, and that trade-off hurts more when the code was written by a machine you need to trust.</p>

<hr />

<h2 id="4-java--spring-boot--convention-as-constraint">#4: Java + Spring Boot — Convention as Constraint</h2>

<p>Wait, didn’t Java just rank #9?</p>

<p>Yes — but “Java” and “Java + Spring Boot” are effectively different languages for LLMs. Spring Boot does at the framework level what our #1 pick does at the language level: it narrows the solution space.</p>

<p>Convention over configuration means the model doesn’t have to choose. <code class="language-plaintext highlighter-rouge">@RestController</code>, <code class="language-plaintext highlighter-rouge">@Service</code>, <code class="language-plaintext highlighter-rouge">@Repository</code>, <code class="language-plaintext highlighter-rouge">@Entity</code> — the layered architecture is so deeply burned into the training data that LLMs produce it reflexively. Spring Boot Starters eliminate the “which library?” question: <code class="language-plaintext highlighter-rouge">spring-boot-starter-web</code> means Tomcat + Jackson + Spring MVC. No debate.</p>

<p>What keeps it from the top 3 is <strong>implicit magic</strong>. <code class="language-plaintext highlighter-rouge">@Transactional</code> looks innocent, but whether the transaction actually works depends on proxy configuration, call origin, and propagation settings — none visible in the code. Auto-configuration is a black box. An LLM generates syntactically perfect annotations that are semantically wrong, and no compiler catches it.</p>

<p><strong>Verdict:</strong> The conventions lift it dramatically over raw Java. But conventions that are <em>implicit</em> will always lose to conventions that are <em>explicit</em> — which is exactly what separates #4 from #1.</p>

<hr />

<h2 id="3-rust--when-it-compiles-its-probably-correct">#3: Rust — When It Compiles, It’s Probably Correct</h2>

<p>Rust has the most seductive promise: if the compiler accepts it, you can trust it. No null pointers, no data races, no buffer overflows. The borrow checker is the ultimate code reviewer, and it doesn’t get tired or distracted.</p>

<p>For AI-generated code, this <em>should</em> be paradise.</p>

<p>In practice, LLMs <em>fight</em> the borrow checker. Not because they don’t know the rules, but because lifetime annotations and ownership decisions require deep architectural understanding across the entire program flow. An LLM generating a single function often lacks that context. The result: <code class="language-plaintext highlighter-rouge">.clone()</code> scattered everywhere, <code class="language-plaintext highlighter-rouge">Arc&lt;Mutex&lt;&gt;&gt;</code> as a crutch, or code that simply doesn’t compile and needs three, four, five iterations.</p>

<p>Rust is the language where AI-generated code is most <em>correct when it works</em>. But the probability of first-attempt compilation is significantly lower than our top 2. And in an AI-assisted workflow, iteration speed matters.</p>

<p><strong>Verdict:</strong> The strongest safety guarantees of any language. The steepest hill for an LLM to climb. If you have patience, the result is worth it. If you want flow, keep reading.</p>

<hr />

<h2 id="2-typescript--brilliant-unreliable-everywhere">#2: TypeScript — Brilliant, Unreliable, Everywhere</h2>

<p>TypeScript combines the largest typed-language training corpus with a genuinely powerful type system. On a good day, LLM-generated TypeScript is the most <em>impressive</em> code on this list — complex React components, perfectly typed API layers, elegant generics.</p>

<p>On a bad day, you get type gymnastics that would make a compiler engineer wince. Union types nested inside conditional types mapped over template literal types. Technically correct. Practically unmaintainable. And the LLM has no sense of when it’s crossing the line from useful typing into academic flex.</p>

<p>The ecosystem doesn’t help. React or Vue or Svelte? Next or Nuxt or Astro? ESM or CommonJS — still, in 2026? <code class="language-plaintext highlighter-rouge">tsconfig.json</code> alone has dozens of options that fundamentally change language behavior. <code class="language-plaintext highlighter-rouge">strict: true</code> and <code class="language-plaintext highlighter-rouge">strict: false</code> are practically different languages.</p>

<p>TypeScript is the brilliant but unreliable colleague. You’ll get flashes of genius and occasional chaos, sometimes in the same file.</p>

<p><strong>Verdict:</strong> Highest ceiling of any language on this list. But the floor is lower than you’d expect, and the variance is the real problem.</p>

<hr />

<h2 id="1-go">#1: Go</h2>

<p>You knew it was coming. Or maybe you didn’t, because Go is nobody’s idea of an exciting language. It doesn’t have generics worth mentioning until recently. No inheritance. No exceptions. No operator overloading. No macros. No magic.</p>

<p><strong>That’s the entire point.</strong></p>

<p>Go wins because it’s the only language where <em>every single dimension that matters for LLM-generated code</em> is strong simultaneously, without any of them undermining the others:</p>

<p><strong>Predictability.</strong> For any given task, there’s essentially one way to write it in Go. Ask five different LLMs to write an HTTP handler, and you get five nearly identical results. That’s not a limitation. That’s what makes AI-generated Go code <em>trustable</em> without reviewing every line.</p>

<p><strong>Formatting.</strong> <code class="language-plaintext highlighter-rouge">go fmt</code>. Solved problem. The model never wastes a single token on style decisions. Every Go file on GitHub looks the same. Every Go file an LLM generates looks the same. Boring. Perfect.</p>

<p><strong>Explicitness.</strong> No annotations that secretly change runtime behavior. No auto-configuration. No dependency injection magic. No decorator sorcery. What you read is what executes. When an LLM writes Go, the semantics are visible in the syntax.</p>

<p><strong>Error handling.</strong> <code class="language-plaintext highlighter-rouge">if err != nil</code> is repetitive, mechanical, and nearly impossible to get wrong. Compare this to exception hierarchies, <code class="language-plaintext highlighter-rouge">try-catch</code> scoping decisions, or Rust’s <code class="language-plaintext highlighter-rouge">?</code> chains that require understanding the full error type tree. Go’s approach is tedious for humans. It’s <em>trivial</em> for machines.</p>

<p><strong>The standard library.</strong> <code class="language-plaintext highlighter-rouge">net/http</code>, <code class="language-plaintext highlighter-rouge">encoding/json</code>, <code class="language-plaintext highlighter-rouge">database/sql</code>, <code class="language-plaintext highlighter-rouge">crypto</code>, <code class="language-plaintext highlighter-rouge">os</code> — you can build serious production software without a single external dependency. Every third-party library is an opportunity for an LLM to hallucinate an API. Go minimizes that surface area.</p>

<p><strong>Language size.</strong> Go’s spec is small enough that an LLM can reliably master <em>all of it</em>, not just the commonly-used subset. With C++, TypeScript, or even Rust, every model has blind spots. With Go, there’s barely anywhere to hide.</p>

<p>Put it all together and you get something no other language offers: <strong>consistent, predictable, correct-on-first-attempt code generation.</strong> Not the most elegant code. Not the most expressive. Not the most impressive on a conference slide.</p>

<p>Just the most <em>reliable.</em></p>

<hr />

<h2 id="the-uncomfortable-takeaway">The Uncomfortable Takeaway</h2>

<p>The language you enjoy writing and the language that produces the most reliable AI-generated code are probably not the same.</p>

<p>Go is verbose, unopinionated about architecture, and deliberately missing features that other languages consider essential. It was designed for large teams of average programmers working on massive codebases at Google. It turns out that “optimized for readability by strangers” maps almost perfectly to “optimized for generation by machines.”</p>

<p>In an era where a growing percentage of production code is AI-generated or AI-assisted, <strong>the best language isn’t the most expressive one — it’s the most predictable one.</strong></p>

<p>And right now, nothing comes close to Go.</p>]]></content><author><name>deemkeen</name><email>dimgur@gmail.com</email></author><category term="programming" /><category term="ai" /><category term="nerdstuff" /><summary type="html"><![CDATA[Every programming language claims to be productive. But in 2026, there’s a new benchmark that matters: how well does an LLM write code in your language?]]></summary></entry><entry><title type="html">I Made a Thing: Blogging Through SSH Because Browsers Are Overrated</title><link href="https://deemkeen.github.io/2025/11/27/i-made-a-thing-blogging-through-ssh/" rel="alternate" type="text/html" title="I Made a Thing: Blogging Through SSH Because Browsers Are Overrated" /><published>2025-11-27T00:00:00+00:00</published><updated>2025-11-27T00:00:00+00:00</updated><id>https://deemkeen.github.io/2025/11/27/i-made-a-thing-blogging-through-ssh</id><content type="html" xml:base="https://deemkeen.github.io/2025/11/27/i-made-a-thing-blogging-through-ssh/"><![CDATA[<p>So I’ve been working on this project called Stegodon for a while now, and I figured it’s time to actually write about it instead of just quietly pushing commits to GitHub at 2 AM.</p>

<p>The basic idea is pretty simple: what if you could blog directly from your terminal via SSH? No browser. No web forms. Just ssh yourdomain.com -p 23232 and you’re writing. And then—because why not—it federates to Mastodon and the rest of the Fediverse through ActivityPub.</p>

<p>Yeah, I know how it sounds. “SSH blogging platform” sounds like something you’d build as a weekend joke project. But here’s the thing: I’ve been using it for my own notes and posts, and it’s actually kind of… nice? There’s something weirdly satisfying about opening a terminal, SSHing into your own server, and just typing. No page load times. No cookies. No “Accept all cookies or read our 47-page privacy policy” banners.</p>

<p>I built the whole TUI with Charm’s bubbletea and lipgloss libraries, which made the terminal interface actually pleasant to look at and use. The shortcuts are tab / shift+tab for navigation because why not, Ctrl+S to post a new note. It felt right pretty quickly, which was a good sign.</p>

<p>The federation part was… interesting. ActivityPub isn’t exactly simple, and getting HTTP signatures right took longer than I’d like to admit. But now you can follow people on Mastodon, they can follow you back, and posts show up where they should. There’s also RSS feeds for each user because RSS should never die, and a web interface for people who insist on using browsers (I know, I know, not everyone lives in their terminal).</p>

<p>Right now you can run it single-user or multi-user, there’s Docker images if you want easy deployment, and it stores everything in SQLite with WAL mode. The code is Go, it’s MIT licensed, and it lives at <a href="https://github.com/deemkeen/stegodon">github.com/deemkeen/stegodon</a></p>

<p>Here’s where I could use some help though. I know the codebase pretty well at this point, but I’m just one person and I’m sure there are rough edges I’m not seeing. If you’re into terminal UIs, federation protocols, Go, or just weird side projects, I’d love to have more eyes on this. Maybe you’ll find a bug I missed. Maybe you’ll have ideas for features I haven’t thought of. Maybe you just want to poke around the code and see how ActivityPub works in practice.</p>

<p>And if you’re someone who likes to vibecode with Claude or ChatGPT or whatever AI tool you prefer—hell yeah, bring that energy. Point your LLM at the codebase and see what interesting things emerge. Sometimes the best contributions come from people experimenting with different approaches, whether you’re writing every line yourself or pair-programming with an AI. Code is code, and good ideas are good ideas.</p>

<p>Some things I’m thinking about for the future: better federation error handling, maybe support for media attachments (though I kind of like the text-only simplicity), possibly improving the web UI, better documentation for self-hosting. But honestly, I’m open to ideas. This whole thing started because I wanted to blog without opening a browser, and it grew from there. Who knows where it should go next?</p>

<p>If you want to try it out, the README has instructions. If you find bugs, open an issue. If you want to contribute, PRs are welcome. If you just want to tell me this is a terrible idea and I should have just used WordPress like a normal person, well, you’re probably right but I built it anyway.</p>

<p>The repo is here: <a href="https://github.com/deemkeen/stegodon">Stegodon</a></p>

<p>I’m curious to see if anyone else finds this useful or interesting. Or if I’m just one of a few oddballs who wanted to SSH into a blogging platform. Either way, the code’s out there now.</p>]]></content><author><name>deemkeen</name><email>dimgur@gmail.com</email></author><category term="ssh" /><category term="fediverse" /><category term="nerdstuff" /><summary type="html"><![CDATA[So I’ve been working on this project called Stegodon for a while now, and I figured it’s time to actually write about it instead of just quietly pushing commits to GitHub at 2 AM.]]></summary></entry><entry><title type="html">Git as Federation Transport — Rethinking How Small Social Networks Talk to Each Other</title><link href="https://deemkeen.github.io/2025/11/18/git-as-federation-transport/" rel="alternate" type="text/html" title="Git as Federation Transport — Rethinking How Small Social Networks Talk to Each Other" /><published>2025-11-18T00:00:00+00:00</published><updated>2025-11-18T00:00:00+00:00</updated><id>https://deemkeen.github.io/2025/11/18/git-as-federation-transport</id><content type="html" xml:base="https://deemkeen.github.io/2025/11/18/git-as-federation-transport/"><![CDATA[<p>For the past weeks I’ve been deep in the weeds of building <a href="https://github.com/deemkeen/stegodon"><strong>stegodon</strong></a>, my tiny, terminal-first microblog server. You write posts entirely over SSH in a TUI, hit <code class="language-plaintext highlighter-rouge">Ctrl+S</code>, and they appear as ActivityPub Notes in the Fediverse.</p>

<p>But a new idea has been stuck in my head:</p>

<h2 id="what-if-federation-didnt-need-http-at-all"><strong>What if federation didn’t need HTTP at all?</strong></h2>

<p>What if servers didn’t push JSON to each other over inbox endpoints, deal with reverse proxies, queues, or retries?</p>

<p>What if we could federate <strong>entirely over Git</strong>?</p>

<p>This post is a first look at that idea — and why I’m seriously considering building an experimental Git-powered federation mode into stegodon.</p>

<hr />

<h1 id="-the-core-idea-git-as-federation-transport">🧠 <strong>The Core Idea: “Git as Federation Transport”</strong></h1>

<p>Here’s the premise:</p>

<blockquote>
  <p>Instead of sending ActivityPub activities over HTTP,
each instance becomes a Git repository.
Posts and activities are stored as Git objects.
Synchronization happens through <code class="language-plaintext highlighter-rouge">git push</code> and <code class="language-plaintext highlighter-rouge">git fetch</code>.</p>
</blockquote>

<p>No HTTP endpoints.
No inbox queues.
No background workers.
Just simple, predictable, text-based replication using Git — the tool developers already trust every day.</p>

<p>Let’s break it down.</p>

<hr />

<h1 id="-1-each-instance--a-git-repository">📦 <strong>1. Each instance = a Git repository</strong></h1>

<p>Every stegodon instance already stores posts as small JSON documents.
Representing them in Git is a natural fit:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>repo/
  users/alice/posts/0012.json
  outbox/0012.json
  follows/bob.txt
  actors/alice.json
</code></pre></div></div>

<p>Each commit becomes an activity: Create, Update, Delete, Follow.</p>

<p>Git gives us:</p>

<ul>
  <li>immutable history</li>
  <li>content addressing</li>
  <li>diffability</li>
  <li>compression</li>
  <li>offline operations</li>
  <li>signing (SSH or GPG)</li>
</ul>

<p>It’s basically <em>social media meets version control</em>.</p>

<hr />

<h1 id="-2-remotes-per-instance-not-per-user">🔌 <strong>2. Remotes per instance, not per user</strong></h1>

<p>Most traditional federation models scale with the number of users.
But Git gives us a better pattern:</p>

<blockquote>
  <p><strong>One Git remote per instance</strong>, not per individual follower.</p>
</blockquote>

<p>So if your server follows three other servers, you have three remotes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>origin     (your own)
bob-social (git@bob.social:stegodon.git)
carol-net  (git@carol.net:stegodon.git)
</code></pre></div></div>

<p>This keeps things lean and scalable — perfect for small, indie, nerdy communities.</p>

<hr />

<h1 id="-3-publishing--git-push">📤 <strong>3. Publishing = <code class="language-plaintext highlighter-rouge">git push</code></strong></h1>

<p>When a user posts something:</p>

<ol>
  <li>stegodon writes a JSON object</li>
  <li>commits it with metadata</li>
  <li>pushes it to follower instances:</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push bob-social main
git push carol-net main
</code></pre></div></div>

<p>No HTTP endpoint at all.
Just Git replication.</p>

<hr />

<h1 id="-4-reading--git-fetch">📥 <strong>4. Reading = <code class="language-plaintext highlighter-rouge">git fetch</code></strong></h1>

<p>To get new posts from people you follow:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch --all
</code></pre></div></div>

<p>Stegodon inspects the new commits, imports the activities, updates the timeline, and merges them into your local storage.</p>

<p>Federation becomes symmetrical:</p>

<blockquote>
  <p>everyone sends and receives via the same transport.</p>
</blockquote>

<hr />

<h1 id="-5-security-integrity--offline-first-for-free">🔒 <strong>5. Security, integrity &amp; offline-first for free</strong></h1>

<p>Because Git is built around cryptographic object IDs and optional signing, you automatically get:</p>

<ul>
  <li>tamper-evident history</li>
  <li>verifiable authorship</li>
  <li>built-in replication</li>
  <li>conflict resolution</li>
  <li>offline posting</li>
  <li>delayed sync</li>
  <li>no single point of failure</li>
</ul>

<p>This is something most social protocols <em>try</em> to design — Git gives it to us out of the box.</p>

<hr />

<h1 id="-why-this-feels-right-for-small-social-networks">🌍 <strong>Why this feels right for small social networks</strong></h1>

<p>This approach isn’t meant to replace ActivityPub.
It’s an <strong>alternative federation mechanism</strong> with a completely different philosophy:</p>

<ul>
  <li>simple</li>
  <li>text-based</li>
  <li>terminal-friendly</li>
  <li>decentralized</li>
  <li>infrastructure-light</li>
  <li>hackable</li>
  <li>inspectable</li>
</ul>

<p>It’s perfect for:</p>

<ul>
  <li>personal servers</li>
  <li>small communities</li>
  <li>self-hosters</li>
  <li>tilde-style networks</li>
  <li>people who prefer SSH over browsers</li>
  <li>experimental and research-driven systems</li>
</ul>

<p>In other words: exactly the people who might enjoy using <strong>stegodon</strong>.</p>

<hr />

<h1 id="-caveats-and-limits">🚧 <strong>Caveats and limits</strong></h1>

<p>This won’t scale to Mastodon-size global federation.
And that’s okay.</p>

<p>The goal is not “a universal protocol for everyone”.
The goal is:</p>

<blockquote>
  <p><strong>A minimal, peer-to-peer, Git-powered social network for small groups of humans who love the terminal.</strong></p>
</blockquote>

<hr />

<h1 id="-whats-next-experimental-implementation">🚀 <strong>What’s next? Experimental Implementation</strong></h1>

<p>I plan to explore this in an <strong>experimental branch</strong> of stegodon:</p>

<ul>
  <li>Git-backed storage for posts</li>
  <li>remotes-per-instance</li>
  <li>push on publish</li>
  <li>periodic or manual fetch</li>
  <li>merge timeline updates</li>
  <li>optional bridge to ActivityPub</li>
</ul>

<p>It will probably be rough at the beginning, but that’s the fun part.</p>

<p>If you like the idea, please reach out, open issues, or share thoughts.
I’d love to hear from people who want to explore Git as a transport layer for social software.</p>

<hr />

<h1 id="-wrapping-up">🐘 <strong>Wrapping up</strong></h1>

<p>Git-based federation is weird.
But it’s also elegant in a way that’s hard to ignore.</p>

<p>It turns blogging and federation into something <em>you can literally <code class="language-plaintext highlighter-rouge">git log</code></em>.
Something you can sync over SSH on a Raspberry Pi.
Something you can fork.</p>

<p>And maybe — just maybe — that’s exactly the kind of simplicity we need more of.</p>]]></content><author><name>deemkeen</name><email>dimgur@gmail.com</email></author><category term="ssh" /><category term="git" /><category term="fediverse" /><category term="nerdstuff" /><summary type="html"><![CDATA[For the past weeks I’ve been deep in the weeds of building stegodon, my tiny, terminal-first microblog server. You write posts entirely over SSH in a TUI, hit Ctrl+S, and they appear as ActivityPub Notes in the Fediverse.]]></summary></entry><entry><title type="html">I Broke My Pop!_OS Boot, Then Fixed It by Deleting One File (With a Little CSI: Linux)</title><link href="https://deemkeen.github.io/2025/11/04/git-init-or-how-i-broke-my-linux/" rel="alternate" type="text/html" title="I Broke My Pop!_OS Boot, Then Fixed It by Deleting One File (With a Little CSI: Linux)" /><published>2025-11-04T00:00:00+00:00</published><updated>2025-11-04T00:00:00+00:00</updated><id>https://deemkeen.github.io/2025/11/04/git-init-or-how-i-broke-my-linux</id><content type="html" xml:base="https://deemkeen.github.io/2025/11/04/git-init-or-how-i-broke-my-linux/"><![CDATA[<p><strong>TL;DR:</strong> I ran <code class="language-plaintext highlighter-rouge">fwupdmgr update --offline</code> because the normal update couldn’t find a file. My machine fell into an infinite “Upgrade complete. Autoremoving old packages…” → reboot loop. I spelunked through TTYs, LUKS, <strong>LVM</strong>, chroot, <code class="language-plaintext highlighter-rouge">dpkg --configure -a</code>, <code class="language-plaintext highlighter-rouge">update-initramfs</code>, <code class="language-plaintext highlighter-rouge">bootctl</code>, and a <code class="language-plaintext highlighter-rouge">/dev/pts</code> heckle. The real culprit? A leftover <strong>systemd offline-update marker</strong>: <code class="language-plaintext highlighter-rouge">/system-update</code>. Deleting it ended the loop.</p>

<hr />

<h2 id="the-setup-just-a-firmware-update-how-bad-could-it-be">The Setup: “Just a Firmware Update, How Bad Could It Be?”</h2>

<p>I wanted to be a responsible adult and update firmware:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>fwupdmgr update
</code></pre></div></div>

<p>It shrugged and said a file couldn’t be found. Fine! I escalated:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>fwupdmgr update <span class="nt">--offline</span>
</code></pre></div></div>

<p>Reboot. Pop!_OS splash. Then:</p>

<blockquote>
  <p><strong>“Upgrade complete. Autoremoving old packages…”</strong></p>
</blockquote>

<p>…and <strong>reboot</strong>. Then again. And again. Linux Groundhog Day.</p>

<hr />

<h2 id="act-i-down-the-rabbit-hole-tty-luks-and-lvm">Act I: Down the Rabbit Hole (TTY, LUKS, and… LVM!)</h2>

<p>I jumped into a recovery shell. First lesson (again): <strong><code class="language-plaintext highlighter-rouge">crypto_LUKS</code> isn’t a filesystem</strong>, it’s a vault. Unlock it, then mount whatever’s inside.</p>

<p>In my case, inside the LUKS container was <strong>LVM</strong>, so the <em>correct</em> mount path was:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-i</span>

<span class="c"># 1) LUKS öffnen</span>
cryptsetup luksOpen /dev/nvme0n1p3 cryptroot

<span class="c"># 2) LVM erkennen und aktivieren</span>
vgscan
vgchange <span class="nt">-ay</span>
lvs      <span class="c"># shows VG/LV names, e.g., pop-os-vg/root</span>

<span class="c"># 3) Root-LV mounten (not the bare cryptroot)</span>
mount /dev/mapper/&lt;VG&gt;-&lt;LV&gt; /mnt
</code></pre></div></div>

<p>Then the standard chroot ballet:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>i <span class="k">in </span>dev proc sys run<span class="p">;</span> <span class="k">do </span>mount <span class="nt">--bind</span> /<span class="nv">$i</span> /mnt/<span class="nv">$i</span><span class="p">;</span> <span class="k">done
</span><span class="nb">chroot</span> /mnt /bin/bash
</code></pre></div></div>

<p>Package spa day:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dpkg <span class="nt">--configure</span> <span class="nt">-a</span>
apt <span class="nt">-f</span> <span class="nb">install
</span>apt update
apt full-upgrade
</code></pre></div></div>

<p>APT heckled me with <strong>“Cannot write log: Is /dev/pts mounted?”</strong>, which is a polite way of saying <em>“mount devpts, please.”</em>
I obliged:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount <span class="nt">-t</span> devpts devpts /dev/pts
</code></pre></div></div>

<hr />

<h2 id="act-ii-suspicious-warnings-and-bootloader-side-quests">Act II: Suspicious Warnings and Bootloader Side Quests</h2>

<p><code class="language-plaintext highlighter-rouge">update-initramfs</code> complained:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>warning: target 'cryptroot' not found in /etc/crypttab
</code></pre></div></div>

<p>I added the proper entry:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptroot UUID=&lt;my-luks-uuid&gt; none luks,discard
</code></pre></div></div>

<p>Then rebuilt:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>update-initramfs <span class="nt">-u</span> <span class="nt">-k</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">ls</span> <span class="nt">-1</span> /lib/modules | <span class="nb">sort</span> <span class="nt">-V</span> | <span class="nb">tail</span> <span class="nt">-1</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>Bootloader courtesy visit:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kernelstub <span class="nt">-v</span>
bootctl update <span class="o">||</span> <span class="nb">true</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">bootctl</code> responded: <em>“Skipping systemd-bootx64.efi since a newer boot loader version exists already.”</em>
That’s normal when your ESP already has a newer bootloader than the chroot environment.</p>

<p>I unmounted like a pro, rebooted…</p>

<p>…and got <strong>“Upgrade complete. Autoremoving old packages…”</strong> → reboot. Again.</p>

<hr />

<h2 id="plot-twist-the-95-second-video-that-solved-everything">Plot Twist: The 9.5-Second Video That Solved Everything</h2>

<p>At this point I recruited ChatGPT and uploaded a short boot video.
It pulled a few frames and spotted the telltale line:</p>

<blockquote>
  <p><strong>“Upgrade complete. Autoremoving old packages…”</strong></p>
</blockquote>

<p>…right before a clean reboot. That pattern screams <strong>systemd offline update mode</strong> still being active. In other words, the marker file that tells systemd <em>“boot into the offline updater”</em> never got cleaned up.</p>

<p>I checked:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> <span class="nt">-l</span> /system-update /etc/system-update
</code></pre></div></div>

<p>There it was: <strong><code class="language-plaintext highlighter-rouge">/system-update</code></strong>, sitting smugly like a tiny chaos gremlin.</p>

<p>I removed it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo rm</span> <span class="nt">-f</span> /system-update /etc/system-update
</code></pre></div></div>

<p>Reboot.
No loop. Desktop. Happiness.</p>

<p><strong>How ChatGPT guessed:</strong> the video showed a successful-looking “Upgrade complete. Autoremoving old packages…” <em>followed immediately by a reboot</em>, with no login ever reached. That’s textbook offline-update behavior: systemd boots into a special target, runs updates, then reboots. If the <strong>marker file</strong> sticks around, it does that <strong>every time</strong>. The frame text + reboot timing was the smoking gun.</p>

<hr />

<h2 id="why-this-works">Why This Works</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">fwupdmgr update --offline</code> uses systemd’s offline-update mechanism.</li>
  <li>systemd looks for <code class="language-plaintext highlighter-rouge">/system-update</code> (or <code class="language-plaintext highlighter-rouge">/etc/system-update</code>). If present, it boots into <code class="language-plaintext highlighter-rouge">system-update.target</code> to run updates before the real boot.</li>
  <li>If the process fails or cleanup never happens, the marker persists → <strong>every boot</strong> repeats the “update + reboot.”</li>
  <li>Deleting the marker lets the system boot normally.</li>
</ul>

<hr />

<h2 id="the-minimal-escape-plan">The Minimal Escape Plan</h2>

<p>If you ever hit the same loop:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Kill the trigger</span>
<span class="nb">sudo rm</span> <span class="nt">-f</span> /system-update /etc/system-update

<span class="c"># (Optional) sanity checks</span>
systemctl get-default
<span class="c"># If it somehow says system-update.target:</span>
<span class="c"># sudo systemctl set-default graphical.target</span>

<span class="c"># (Optional) finish any pending package business</span>
<span class="nb">sudo </span>dpkg <span class="nt">--configure</span> <span class="nt">-a</span>
<span class="nb">sudo </span>apt <span class="nt">-f</span> <span class="nb">install
sudo </span>apt autoremove <span class="nt">--purge</span> <span class="nt">-y</span>
</code></pre></div></div>

<p>If you’re on LUKS+LVM and need to chroot again, remember the <strong>LVM</strong> bit:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptsetup luksOpen /dev/nvme0n1p3 cryptroot
vgscan <span class="o">&amp;&amp;</span> vgchange <span class="nt">-ay</span> <span class="o">&amp;&amp;</span> lvs
mount /dev/mapper/&lt;VG&gt;-&lt;LV&gt; /mnt
<span class="k">for </span>i <span class="k">in </span>dev proc sys run<span class="p">;</span> <span class="k">do </span>mount <span class="nt">--bind</span> /<span class="nv">$i</span> /mnt/<span class="nv">$i</span><span class="p">;</span> <span class="k">done
</span><span class="nb">chroot</span> /mnt /bin/bash
</code></pre></div></div>

<hr />

<h2 id="lessons-gently-roasted">Lessons (Gently Roasted)</h2>

<ul>
  <li>“Offline” means <em>really</em> offline; don’t interrupt it.</li>
  <li><code class="language-plaintext highlighter-rouge">crypto_LUKS</code> ≠ filesystem. Inside may be LVM; mount the <strong>LV</strong>, not the raw mapper.</li>
  <li><code class="language-plaintext highlighter-rouge">Is /dev/pts mounted?</code> is more of a vibe check than a catastrophe.</li>
  <li>A single file—<strong><code class="language-plaintext highlighter-rouge">/system-update</code></strong>—can keep your OS circling like a Roomba.</li>
</ul>

<hr />

<h2 id="epilogue">Epilogue</h2>

<p>I wanted a firmware update. I got a crash course in LUKS, LVM, initramfs, and systemd’s secret handshake. One tiny marker file caused a very big loop; one tiny <code class="language-plaintext highlighter-rouge">rm -f</code> made me feel like a wizard.</p>

<p>If this saved you from the “Upgrade complete. Autoremoving old packages…” purgatory, treat yourself to a coffee. Future you says thanks.</p>]]></content><author><name>deemkeen</name><email>dimgur@gmail.com</email></author><category term="linux" /><category term="popos" /><category term="nerdstuff" /><summary type="html"><![CDATA[TL;DR: I ran fwupdmgr update --offline because the normal update couldn’t find a file. My machine fell into an infinite “Upgrade complete. Autoremoving old packages…” → reboot loop. I spelunked through TTYs, LUKS, LVM, chroot, dpkg --configure -a, update-initramfs, bootctl, and a /dev/pts heckle. The real culprit? A leftover systemd offline-update marker: /system-update. Deleting it ended the loop.]]></summary></entry></feed>