PageSourceSearch

http://aly.codes/blog/2026-06-20-appherder/

html aly.codes collected 2026-10-02 00:14:56 UTC 40,464 bytes, 85 lines download raw bytes

1<!DOCTYPE html><html lang="en" class="bg-pink-100 text-zinc-900"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="https://cdn.bsky.app/img/avatar/plain/did:plc:zntngpowgd6rorjt3haywj36/bafkreihbcqohzdhmoupnaeztrbvtzzwgxdzaus4rnswsse7oxvudnwngry"/><link rel="preload" as="image" href="https://video.bsky.app/watch/did%3Aplc%3Azntngpowgd6rorjt3haywj36/bafkreicwn7eoabhcfw6tkqpmcfckqfq7fyxnd4fi6vt7mhmn2egvneym4i/thumbnail.jpg"/><title>Making AppImages suck less with AppHerder · Aly Raffauf</title><meta name="description" content="Lifecycle management for your AppImages."/><meta name="at:author" content="at://did:plc:zntngpowgd6rorjt3haywj36"/><meta name="at:me" content="at://did:plc:zntngpowgd6rorjt3haywj36"/><meta name="at:canonical" content="at://did:plc:zntngpowgd6rorjt3haywj36/site.standard.document/3mooywkcs5s22"/><meta property="og:type" content="article"/><meta property="og:site_name" content="Aly Raffauf"/><meta property="og:title" content="Making AppImages suck less with AppHerder"/><meta property="og:description" content="Lifecycle management for your AppImages."/><meta property="og:url" content="https://aly.codes/blog/2026-06-20-appherder/"/><meta property="article:published_time" content="2026-06-20"/><meta name="twitter:card" content="summary_large_image"/><meta name="twitter:title" content="Making AppImages suck less with AppHerder"/><meta name="twitter:description" content="Lifecycle management for your AppImages."/><link rel="canonical" href="https://aly.codes/blog/2026-06-20-appherder/"/><link rel="alternate" type="application/rss+xml" href="/rss.xml"/><link rel="icon" type="image/svg+xml" href="/favicon.svg"/><link rel="site.standard.publication" href="at://did:plc:zntngpowgd6rorjt3haywj36/site.standard.publication/3m36bacisus2x"/><link rel="site.standard.document" href="at://did:plc:zntngpowgd6rorjt3haywj36/site.standard.document/3mooywkcs5s22"/><link rel="modulepreload" href="/assets/manifest-5e6941bc.js"/><link rel="modulepreload" href="/assets/entry.client-CzlZ8_4y.js"/><link rel="modulepreload" href="/assets/jsx-runtime-DuYa2U6n.js"/><link rel="modulepreload" href="/assets/errorBoundaries-CVWeEsF_.js"/><link rel="modulepreload" href="/assets/preload-helper-COzTzGja.js"/><link rel="modulepreload" href="/assets/root-mZbo6xxw.js"/><link rel="modulepreload" href="/assets/lib-BeTON2b2.js"/><link rel="modulepreload" href="/assets/createLucideIcon-S_p9Qgj6.js"/><link rel="modulepreload" href="/assets/atproto-BpasDYB3.js"/><link rel="modulepreload" href="/assets/post-HAP-0D4Z.js"/><link rel="modulepreload" href="/assets/client-DNuoOhvn.js"/><link rel="modulepreload" href="/assets/fa6-XTdwxlmf.js"/>
1<script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","@id":"https://aly.codes/#aly-raffauf","name":"Aly Raffauf","url":"https://aly.codes/","image":"https://aly.codes/profile.jpg","jobTitle":"Software engineer","sameAs":["https://github.com/alyraffauf","https://linkedin.com/in/alyraffauf","https://bsky.app/profile/aly.codes","https://tangled.org/aly.codes"]}</script>
1<link rel="stylesheet" href="/assets/root-DtBmjCmA.css"/></head><body class="font-sans antialiased"><div class="fixed inset-0 pointer-events-none z-[-1] overflow-hidden"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose absolute -top-4 -left-4 w-24 h-24 md:w-48 md:h-48 text-rose-300/70 rotate-[-30deg] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose absolute -top-2 -right-6 w-24 h-24 md:w-48 md:h-48 text-rose-200/80 rotate-30 scale-x-[-1] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose absolute -bottom-4 -left-2 w-24 h-24 md:w-48 md:h-48 text-rose-200/80 rotate-[-30deg] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose absolute -bottom-6 -right-4 w-24 h-24 md:w-48 md:h-48 text-rose-300/70 rotate-30 scale-x-[-1] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose hidden md:block md:absolute top-1/4 -left-8 w-24 h-24 text-rose-300/80 rotate-25 scale-x-[-1] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose hidden md:block md:absolute top-1/2 -left-6 w-28 h-28 text-rose-200/90 rotate-[-35deg] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose hidden md:block md:absolute top-3/4 -left-4 w-20 h-20 text-rose-300/80 rotate-40 scale-x-[-1] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose hidden md:block md:absolute top-1/4 -right-8 w-24 h-24 text-rose-300/80 rotate-[-25deg] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose hidden md:block md:absolute top-1/2 -right-6 w-28 h-28 text-rose-200/90 rotate-35 scale-x-[-1] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rose hidden md:block md:absolute top-3/4 -right-4 w-20 h-20 text-rose-300/80 rotate-[-40deg] animate-pulse-subtle" aria-hidden="true"><path d="M17 10h-1a4 4 0 1 1 4-4v.534"></path><path d="M17 6h1a4 4 0 0 1 1.42 7.74l-2.29.87a6 6 0 0 1-5.339-10.68l2.069-1.31"></path><path d="M4.5 17c2.8-.5 4.4 0 5.5.8s1.8 2.2 2.3 3.7c-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2"></path><path d="M9.77 12C4 15 2 22 2 22"></path><circle cx="17" cy="8" r="2"></circle></svg></div><main class="min-h-screen max-w-3xl mx-auto p-8 text-zinc-900"><header class="mb-12 flex items-center justify-between"><div class="text-4xl font-bold"><a href="/" data-discover="true">Aly Raffauf</a></div><nav class="hidden gap-4 md:flex"><a class="mr-4 hover:underline" href="/" data-discover="true">Home</a><a class="mr-4 hover:underline" href="/blog" data-discover="true">Blog</a><a class="mr-4 hover:underline" href="/projects" data-discover="true">Projects</a></nav><div class="md:hidden"><button class="text-2xl" aria-label="Toggle menu">☰</button></div></header><article>
1<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"Making AppImages suck less with AppHerder","description":"Lifecycle management for your AppImages.","datePublished":"2026-06-20","url":"https://aly.codes/blog/2026-06-20-appherder/","mainEntityOfPage":"https://aly.codes/blog/2026-06-20-appherder/","author":{"@id":"https://aly.codes/#aly-raffauf","@type":"Person","name":"Aly Raffauf"}}</script>
1<h1 class="mb-2 text-2xl font-semibold">Making AppImages suck less with AppHerder</h1><p class="mb-8 text-sm text-zinc-600">2026-06-20</p><div class="prose max-w-none prose-a:text-rose-700 prose-a:hover:underline prose-code:text-zinc-800 prose-pre:bg-rose-300 prose-pre:text-zinc-100 prose-hr:border-2 prose-hr:border-rose-300 prose-blockquote:border-l-2 prose-blockquote:border-rose-300 prose-li:marker:text-rose-400"><p>Linux has many ways to install an app, which is a polite way of saying nobody agrees on one.</p>
2<p>Flatpak, Nix, Homebrew, distro packages, and AppImages all have their place in theory, but in practice you are not choosing a format. You are choosing an app, and the app shows up in whatever format the author and your distribution chose to make it available. Sometimes, that format <em>sucks</em>.</p>
3<hr/>
4<div class="not-prose my-6 flex flex-col gap-3 rounded-lg border-2 border-rose-200 bg-rose-50 p-4"><a href="https://bsky.app/profile/aly.codes/post/3m3avs7e2bk2h" target="_blank" rel="noopener noreferrer" class="flex flex-col gap-3 no-underline"><div class="flex items-center gap-3"><img src="https://cdn.bsky.app/img/avatar/plain/did:plc:zntngpowgd6rorjt3haywj36/bafkreihbcqohzdhmoupnaeztrbvtzzwgxdzaus4rnswsse7oxvudnwngry" alt="aly" width="40" height="40" class="rounded-full"/><div class="flex flex-col leading-tight"><span class="font-semibold text-zinc-900">aly</span><span class="text-sm text-zinc-500">@<!-- -->aly.codes</span></div><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" class="ml-auto h-5 w-5 shrink-0 text-rose-400" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z"></path></svg></div><p class="whitespace-pre-line text-zinc-800">i will literally pay the subway builder dev to create a flatpak this appimage situation is so bad</p></a><a href="https://bsky.app/profile/aly.codes/post/3m3avs7e2bk2h" target="_blank" rel="noopener noreferrer" class="flex items-center gap-4 text-sm text-zinc-500 no-underline"><span>Oct 15, 2025</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-heart h-4 w-4" aria-hidden="true"><path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"></path></svg> <!-- -->5</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-repeat2 lucide-repeat-2 h-4 w-4" aria-hidden="true"><path d="m2 9 3-3 3 3"></path><path d="M13 18H7a2 2 0 0 1-2-2V6"></path><path d="m22 15-3 3-3-3"></path><path d="M11 6h6a2 2 0 0 1 2 2v10"></path></svg> <!-- -->0</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle h-4 w-4" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg> <!-- -->1</span></a></div>
5<p>I&#x27;m going to focus on AppImages here because they occupy a particularly annoying middle ground in this crowded field. They are not quite packages, not quite portable apps, and not quite installers. An AppImage is basically an executable runtime with a compressed filesystem glued onto it. That filesystem contains the application, its libraries, and usually a <code>.desktop</code> file and icon. When you run the AppImage, the runtime mounts or extracts that payload and launches the app inside it.</p>
6<p>They are executable filesystem bundles with just enough desktop-adjacent metadata to look like apps, but not enough glue to behave like installed software. That&#x27;s a clever trick, but it also explains most of the weirdness.</p>
7<p>The appeal is obvious. No root access needed, no distro-specific package, and no waiting for a distro maintainer to package your app. For self
7-distributing developers, AppImages are extremely convenient: build one file, throw it at users, and avoid most of the infrastructure and politics around app stores and distro repositories.</p>
8<p>The catch is that running self-contained apps and installing them are not the same thing. An app is not installed just because your kernel agrees to execute it.</p>
9<p>Users have higher expectations. They expect installed apps to:</p>
10<ul>
11<li>show up in the app launcher</li>
12<li>have the right icon</li>
13<li>open files they support</li>
14<li>update without sending you spelunking through GitHub releases</li>
15<li>uninstall without leaving a sad little <code>.desktop</code> file pointing at something that no longer exists</li>
16</ul>
17<p>On their own, AppImages do not provide that. They do not reliably integrate with your desktop. They do not reliably update themselves. Many are unsigned. There is no central registry. There may be embedded update info, or there may not. The payload might be SquashFS, DwarFS, or some hitherto unknown format. Some tools eventually fall back to executing the AppImage itself with <code>--appimage-extract</code>, which is a pretty funny thing to do to a file you are trying to inspect safely.</p>
18<p>To be fair to AppImages, every other platform has goofy app formats, too.</p>
19<p>macOS DMGs make you drag an icon into another icon in a goofy little ritual, but at least the app lands somewhere obvious and everything else is handled for you. Portable Windows EXEs are loose files with no real promises, though nobody is pretending otherwise. Anything more complicated or permanent needs its own bespoke wizard, which is something Linux has <em>mostly</em> avoided.</p>
20<p>That said, I don&#x27;t particularly <em>like</em> AppImages. But they solve a genuine problem and fill a small niche that traditional package managers and modern stores can&#x27;t. AppImages offer clever dependency management and app distribution without the adult supervision of Flatpak or extended timelines of distro packages. That means no built-in sandboxing, no vetting, and no centralized rules, but that&#x27;s kind of the point. It&#x27;s the wild west of package formats.</p>
21<p>And like any wild west, the freedom cuts both ways. AppImages are censorship-resistant because distribution is fractured, decentralized, and piecemeal. That same chaos enables a great deal of laziness and danger. Tradeoffs abound.</p>
22<hr/>
23<h2>AppHerder</h2>
24<p>I was recently stuck on a hopelessly delayed plane for over three hours longer than planned. With no internet and too much time, I wrote the first version of AppHerder: a Python script doing its best to bring order to the chaos of my AppImages.</p>
25<p>Since then, I&#x27;ve dedicated a few nights to producing a more refined version in Go, available as an AppImage on <a href="https://github.com/alyraffauf/appherder">GitHub</a>.</p>
26<p>AppHerder is a local <em>lifecycle manager</em> for AppImages. It does not try to be an app store. It does not solve discovery. It does not decide what software should exist. It&#x27;s barely a package manager. It just takes the AppImages you already have and makes them behave as you&#x27;d expect.</p>
27<p>Here&#x27;s the gist: <code>~/AppImages</code> becomes the source of truth, just like <code>/Applications</code> on macOS. Throw an AppImage in there and AppHerder adopts it. It reads the metadata, installs the icon, writes a patched <code>.desktop</code> file, and marks that launcher as its own with <code>X-AppHerder=true</code>. Delete the AppImage later and AppHerder cleans up the launcher, icon, and metadata it created.</p>
28<p>The trick that makes this feel automatic is a systemd user path unit that watches <code>~/AppImages</code>. When that directory changes, systemd runs <code>appherder sync</code> as a one-shot user service. There is no always-on daemon sitting around waiting.</p>
29<p>To the user, this is basically transparent. Drop an app in the folder and within seconds it&#x27;s in your menu. Delete it and it disappears just as fast.</p>
30<div class="not-prose my-6 flex flex-col gap-3 rounded-lg border-2 border-rose-200 bg-rose-50 p-4"><a href="https://bsky.app/profile/aly.codes/post/3mojhvxap7c2u" target="_blank" rel="noopener noreferrer" class="flex flex-col gap-3 no-underline"><div class="flex items-center gap-3"><img src="https://cdn.bsky.app/img/avatar/plain/did:plc:zntngpowgd6rorjt3haywj36/bafkreihbcqohzdhmoupnaeztrbvtzzwgxdzaus4rnswsse7oxvudnwngry" alt="aly" width="40" height="40" class="rounded-full"/><div class="flex flex-col leading-tight"><span class="font-semibold text-zinc-900">aly</span><span class="text-sm text-zinc-500">@<!-- -->aly.codes</span></div><svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" class="ml-auto h-5 w-5 shrink-0 text-rose-400" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M111.8 62.2C170.2 105.9 233 194.7 256 242.4c23-47.6 85.8-136.4 144.2-180.2c42.1-31.6 110.3-56 110.3 21.8c0 15.5-8.9 130.5-14.1 149.2C478.2 298 412 314.6 353.1 304.5c102.9 17.5 129.1 75.5 72.5 133.5c-107.4 110.2-154.3-27.6-166.3-62.9l0 0c-1.7-4.9-2.6-7.8-3.3-7.8s-1.6 3-3.3 7.8l0 0c-12 35.3-59 173.1-166.3 62.9c-56.5-58-30.4-116 72.5-133.5C100 314.6 33.8 298 15.7 233.1C10.4 214.4 1.5 99.4 1.5 83.9c0-77.8 68.2-53.4 110.3-21.8z"></path></svg></div><p class="whitespace-pre-line text-zinc-800">I think I have successfully &#x27;fixed&#x27; the UX of appimages
31
32this started as a rawdogged (no internet no AI no nothing) python script I hallucinated in one shot on the plane (I spent like 8hrs on planes recently), but I think I think I&#x27;ve eliminated basically all the pain points
33
34(and rewrote it in Go)</p></a><button type="button" class="relative block w-full overflow-hidden rounded-md"><img src="https://video.bsky.app/watch/did%3Aplc%3Azntngpowgd6rorjt3haywj36/bafkreicwn7eoabhcfw6tkqpmcfckqfq7fyxnd4fi6vt7mhmn2egvneym4i/thumbnail.jpg" alt="" width="600" height="340" class="w-full object-cover"/>
34<span class="absolute inset-0 flex items-center justify-center bg-black/20"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-play h-12 w-12 fill-white text-white" aria-hidden="true"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"></path></svg></span></button><a href="https://bsky.app/profile/aly.codes/post/3mojhvxap7c2u" target="_blank" rel="noopener noreferrer" class="flex items-center gap-4 text-sm text-zinc-500 no-underline"><span>Jun 17, 2026</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-heart h-4 w-4" aria-hidden="true"><path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"></path></svg> <!-- -->30</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-repeat2 lucide-repeat-2 h-4 w-4" aria-hidden="true"><path d="m2 9 3-3 3 3"></path><path d="M13 18H7a2 2 0 0 1-2-2V6"></path><path d="m22 15-3 3-3-3"></path><path d="M11 6h6a2 2 0 0 1 2 2v10"></path></svg> <!-- -->1</span><span class="flex items-center gap-1"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle h-4 w-4" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg> <!-- -->4</span></a></div>
35<p>AppHerder uses the same systemd-user approach for automatic upgrades, with a timer instead of a path watcher. Because AppImages have no universal update mechanism, it is very easy to collect stale binaries that need to be manually replaced.</p>
36<p>Luckily, many AppImages provide basic update info in an embedded <code>.upd_info</code> section of the binary. AppHerder checks that first, then falls back to user-configurable sources when upstream metadata is missing or wrong.</p>
37<p>For safer updates, AppImages can be signed to verify provenance. Not every AppImage is signed, because of course they aren&#x27;t, but AppHerder enforces signatures for updates when available. If an AppImage is signed, AppHerder pins that signing key on first install and refuses unsigned or differently-signed updates after that. Unsigned apps still work, but they do not get to pretend they have the same provenance story as signed ones. It is up to the user to decide if they are comfortable using unsigned software.</p>
38<pre><code class="language-text">download candidate
39        |
40        v
41is it an AppImage?
42   | yes
43   v
44expected hash available?
45   | yes -&gt; must match
46   | no  -&gt; continue, but no checksum claim
47   v
48signature available?
49   | yes -&gt; pin key on first install
50   |        require same key for updates
51   | no  -&gt; allowed, but provenance is weaker
52   v
53install / reject
54</code></pre>
55<p>Regardless, AppHerder still verifies that the file is <em>actually</em> an AppImage and checks expected hashes when the update source provides them (spoiler: they do not always provide them). This verification happens before AppHerder installs the update.</p>
56<p>Some AppImage tools execute the AppImage with <code>--appimage-extract</code> to inspect its contents, but AppHerder unpacks them passively without execution. Yes, the user may eventually run the app, but that is a conscious user choice, not something happening automatically just because a manager wanted an icon. Technically speaking, this limits AppHerder to SquashFS and DwarFS AppImages, but that is the <em>vast</em> majority of all AppImages currently available.</p>
57<p>And because updates can always turn into &quot;oops, everything is worse now,&quot;
57 AppHerder keeps several previous versions around for easy rollback.</p>
58<hr/>
59<p>AppHerder does not make AppImages perfect. They&#x27;re not as tidy as Flatpaks or as vetted as your distro&#x27;s packages. They are still AppImages. Sorry.</p>
60<p>Instead, it is an adversarial attempt at making the bargain a lot less bad. If the app you need ships as an AppImage, fine. Put it in <code>~/AppImages</code>, let it behave like an app, and delete it when you&#x27;re done. That should not leave you with a Downloads folder full of mystery binaries and desktop entries pointing at ghosts.</p>
61<p>You can grab AppHerder from <a href="https://github.com/alyraffauf/appherder">GitHub</a>, or use the install script:</p>
62<pre><code class="language-bash">curl -sSL https://raw.githubusercontent.com/alyraffauf/appherder/main/scripts/install.sh | bash
63</code></pre>
64<p>It downloads the latest AppImage, installs AppHerder into <code>~/AppImages</code>, and enables automatic sync and upgrades. After that, <code>~/AppImages</code> is the place: drop AppImages in there, delete them when you&#x27;re done, and let AppHerder handle the boring parts.</p>
65<p>With any luck, this makes them suck <em>less</em>. Regrettably, that may be as good as it gets.</p></div><section class="not-prose mt-12"><h2 class="mb-4 flex items-center gap-2 text-xl font-semibold"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle h-5 w-5 text-rose-400" aria-hidden="true"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path></svg>Mentions</h2><p class="text-sm text-zinc-500">Loading mentions…</p></section></article></main>
65<script>((storageKey, restoreKey) => {
66    if (!window.history.state || !window.history.state.key) {
67      let key = Math.random().toString(32).slice(2);
68      window.history.replaceState({ key }, "");
69    }
70    try {
71      let storedY = JSON.parse(sessionStorage.getItem(storageKey) || "{}")[restoreKey || window.history.state.key];
72      if (typeof storedY === "number")
73        window.scrollTo(0, storedY);
74    } catch (error) {
75      console.error(error);
76      sessionStorage.removeItem(storageKey);
77    }
78  })("react-router-scroll-positions", null)</script>
78<script>window.__reactRouterContext = {"basename":"/","future":{"unstable_enableNodeReadableStream":false,"unstable_optimizeDeps":false},"routeDiscovery":{"mode":"initial"},"ssr":false,"isSpaMode":false};window.__reactRouterContext.stream = new ReadableStream({start(controller){window.__reactRouterContext.streamController = controller;}}).pipeThrough(new TextEncoderStream());</script>
78<script type="module" async="">import "/assets/manifest-5e6941bc.js";
79import * as route0 from "/assets/root-mZbo6xxw.js";
80import * as route1 from "/assets/post-HAP-0D4Z.js";
81  
82  window.__reactRouterRouteModules = {"root":route0,"routes/post":route1};
83
84import("/assets/entry.client-CzlZ8_4y.js");</script>
84<!--$-->
84<script>window.__reactRouterContext.streamController.enqueue("[{\"_1\":2,\"_3\":-5,\"_4\":-5},\"loaderData\",{\"_5\":6},\"actionData\",\"errors\",\"routes/post\",{\"_7\":8,\"_9\":10},\"post\",{\"_49\":50,\"_51\":52,\"_53\":54,\"_55\":56,\"_57\":58,\"_59\":-5,\"_60\":61,\"_62\":63,\"_64\":65},\"blueskyPosts\",[11,12],[42,43],[13,14],\"at://did:plc:zntngpowgd6rorjt3haywj36/app.bsky.feed.post/3mojhvxap7c2u\",{\"_15\":13,\"_16\":17,\"_18\":19,\"_20\":21,\"_22\":23,\"_24\":25,\"_26\":27,\"_28\":29},\"uri\",\"author\",{\"_36\":37,\"_38\":39,\"_40\":41},\"text\",\"I think I have successfully 'fixed' the UX of appimages\\n\\nthis started as a rawdogged (no internet no AI no nothing) python script I hallucinated in one shot on the plane (I spent like 8hrs on planes recently), but I think I think I've eliminated basically all the pain points\\n\\n(and rewrote it in Go)\",\"createdAt\",\"2026-06-17T23:32:08.314Z\",\"likeCount\",30,\"repostCount\",1,\"replyCount\",4,\"embed\",{\"_30\":31,\"_32\":33,\"_34\":35},\"type\",\"video\",\"thumbnail\",\"https://video.bsky.app/watch/did%3Aplc%3Azntngpowgd6rorjt3haywj36/bafkreicwn7eoabhcfw6tkqpmcfckqfq7fyxnd4fi6vt7mhmn2egvneym4i/thumbnail.jpg\",\"playlist\",\"https://video.bsky.app/watch/did%3Aplc%3Azntngpowgd6rorjt3haywj36/bafkreicwn7eoabhcfw6tkqpmcfckqfq7fyxnd4fi6vt7mhmn2egvneym4i/playlist.m3u8\",\"handle\",\"aly.codes\",\"displayName\",\"aly\",\"avatar\",\"https://cdn.bsky.app/img/avatar/plain/did:plc:zntngpowgd6rorjt3haywj36/bafkreihbcqohzdhmoupnaeztrbvtzzwgxdzaus4rnswsse7oxvudnwngry\",\"at://did:plc:zntngpowgd6rorjt3haywj36/app.bsky.feed.post/3m3avs7e2bk2h\",{\"_15\":42,\"_16\":44,\"_18\":45,\"_20\":46,\"_22\":47,\"_24\":48,\"_26\":25,\"_28\":-5},{\"_36\":37,\"_38\":39,\"_40\":41},\"i will literally pay the subway builder dev to create a flatpak this appimage situation is so bad\",\"2025-10-15T18:47:56.406Z\",5,0,\"slug\",\"2026-06-20-appherder\",\"title\",\"Making AppImages suck less with AppHerder\",\"date\",\"2026-06-20\",\"description\",\"Lifecycle management for your AppImages.\",\"content\",\"\\nLinux has many ways to install an app, which is a polite way of saying nobody agrees on one.\\n\\nFlatpak, Nix, Homebrew, distro packages, and AppImages all have their place in theory, but in practice you are not choosing a format. You are cho
84osing an app, and the app shows up in whatever format the author and your distribution chose to make it available. Sometimes, that format _sucks_.\\n\\n---\\n\\n```bsky\\nat://did:plc:zntngpowgd6rorjt3haywj36/app.bsky.feed.post/3m3avs7e2bk2h\\n```\\n\\nI'm going to focus on AppImages here because they occupy a particularly annoying middle ground in this crowded field. They are not quite packages, not quite portable apps, and not quite installers. An AppImage is basically an executable runtime with a compressed filesystem glued onto it. That filesystem contains the application, its libraries, and usually a `.desktop` file and icon. When you run the AppImage, the runtime mounts or extracts that payload and launches the app inside it.\\n\\nThey are executable filesystem bundles with just enough desktop-adjacent metadata to look like apps, but not enough glue to behave like installed software. That's a clever trick, but it also explains most of the weirdness.\\n\\nThe appeal is obvious. No root access needed, no distro-specific package, and no waiting for a distro maintainer to package your app. For self-distributing developers, AppImages are extremely convenient: build one file, throw it at users, and avoid most of the infrastructure and politics around app stores and distro repositories.\\n\\nThe catch is that running self-contained apps and installing them are not the same thing. An app is not installed just because your kernel agrees to execute it.\\n\\nUsers have higher expectations. They expect installed apps to:\\n\\n- show up in the app launcher\\n- have the right icon\\n- open files they support\\n- update without sending you spelunking through GitHub releases\\n- uninstall without leaving a sad little `.desktop` file pointing at something that no longer exists\\n\\nOn their own, AppImages do not provide that. They do not reliably integrate with your desktop. They do not reliably update themselves. Many are unsigned. There is no central registry. There may be embedded update info, or there may not. The payload might be SquashFS, DwarFS, or some hitherto unknown format. Some tools eventually fall back to executing the AppImage itself with `--appimage-extract`, which is a pretty funny thing to do to a file you are trying to inspect safely.\\n\\nTo be fair to AppImages, every other platform has goofy app formats, too.\\n\\nmacOS DMGs make you drag an icon into another icon in a goofy little ritual, but at least the app lands somewhere obvious and everything else is handled for you. Portable Windows EXEs are loose files with no real promises, though nobody is pretending otherwise. Anything more complicated or permanent needs its own bespoke wizard, which is something Linux has _mostly_ avoided.\\n\\nThat said, I don't particularly _like_ AppImages. But they solve a genuine problem and fill a small niche that traditional package managers and modern stores can't. AppImages offer clever dependency management and app distribution without the adult supervision of Flatpak or extended timelines of distro packages. That means no built-in sandboxing, no vetting, and no centralized rules, but that's kind of the point. It's the wild west of package formats.\\n\\nAnd like any wild west, the freedom cuts both ways. AppImages are censorship-resistant because distribution is fractured, decentralized, and piecemeal. That same chaos enables a great deal of laziness and danger. Tradeoffs abound.\\n\\n---\\n\\n## AppHerder\\n\\nI was recently stuck on a hopelessly delayed plane for over three hours longer than planned. With no internet and too much time, I wrote the first version of AppHerder: a Python script doing its best to bring order to the chaos of my AppImages.\\n\\nSince then, I've dedicated a few nights to producing a more refined version in Go, available as an AppImage on [GitHub](https://github.com/alyraffauf/appherder).\\n\\nAppHerder is a local _lifecycle manager_ for AppImages. It does not try to be an app store. It does not solve discovery. It does not decide what software should exist. It's barely a package manager. It just takes the AppImages you already have and makes them behave as you'd expect.\\n\\nHere's the gist: `~/AppImages` becomes the source of truth, just like `/Applications` on macOS. Throw an AppImage in there and AppHerder adopts it. It reads the metadata, installs the icon, writes a patched `.desktop` file, and marks that launcher as its own with `X-AppHerder=true`. Delete the AppImage later and AppHerder cleans up the launcher, icon, and metadata it created.\\n\\nThe trick that makes this feel automatic is a systemd user path unit that watches `~/AppImages`. When that directory changes, systemd runs `appherder sync` as a one-shot user service. There is no always-on daemon sitting around waiting.\\n\\nTo the user, this is basically transparent. Drop an app in the folder and within seconds 
84it's in your menu. Delete it and it disappears just as fast.\\n\\n```bsky\\nat://did:plc:zntngpowgd6rorjt3haywj36/app.bsky.feed.post/3mojhvxap7c2u\\n```\\n\\nAppHerder uses the same systemd-user approach for automatic upgrades, with a timer instead of a path watcher. Because AppImages have no universal update mechanism, it is very easy to collect stale binaries that need to be manually replaced.\\n\\nLuckily, many AppImages provide basic update info in an embedded `.upd_info` section of the binary. AppHerder checks that first, then falls back to user-configurable sources when upstream metadata is missing or wrong.\\n\\nFor safer updates, AppImages can be signed to verify provenance. Not every AppImage is signed, because of course they aren't, but AppHerder enforces signatures for updates when available. If an AppImage is signed, AppHerder pins that signing key on first install and refuses unsigned or differently-signed updates after that. Unsigned apps still work, but they do not get to pretend they have the same provenance story as signed ones. It is up to the user to decide if they are comfortable using unsigned software.\\n\\n```text\\ndownload candidate\\n        |\\n        v\\nis it an AppImage?\\n   | yes\\n   v\\nexpected hash available?\\n   | yes -\u003e must match\\n   | no  -\u003e continue, but no checksum claim\\n   v\\nsignature available?\\n   | yes -\u003e pin key on first install\\n   |        require same key for updates\\n   | no  -\u003e allowed, but provenance is weaker\\n   v\\ninstall / reject\\n```\\n\\nRegardless, AppHerder still verifies that the file is _actually_ an AppImage and checks expected hashes when the update source provides them (spoiler: they do not always provide them). This verification happens before AppHerder installs the update.\\n\\nSome AppImage tools execute the AppImage with `--appimage-extract` to inspect its contents, but AppHerder unpacks them passively without execution. Yes, the user may eventually run the app, but that is a conscious user choice, not something happening automatically just because a manager wanted an icon. Technically speaking, this limits AppHerder to SquashFS and DwarFS AppImages, but that is the _vast_ majority of all AppImages currently available.\\n\\nAnd because updates can always turn into \\\"oops, everything is worse now,\\\" AppHerder keeps several previous versions around for easy rollback.\\n\\n---\\n\\nAppHerder does not make AppImages perfect. They're not as tidy as Flatpaks or as vetted as your distro's packages. They are still AppImages. Sorry.\\n\\nInstead, it is an adversarial attempt at making the bargain a lot less bad. If the app you need ships as an AppImage, fine. Put it in `~/AppImages`, let it behave like an app, and delete it when you're done. That should not leave you with a Downloads folder full of mystery binaries and desktop entries pointing at ghosts.\\n\\nYou can grab AppHerder from [GitHub](https://github.com/alyraffauf/appherder), or use the install script:\\n\\n```bash\\ncurl -sSL https://raw.githubusercontent.com/alyraffauf/appherder/main/scripts/install.sh | bash\\n```\\n\\nIt downloads the latest AppImage, installs AppHerder into `~/AppImages`, and enables automatic sync and upgrades. After that, `~/AppImages` is the place: drop AppImages in there, delete them when you're done, and let AppHerder handle the boring parts.\\n\\nWith any luck, this makes them suck _less_. Regrettably, that may be as good as it gets.\\n\",\"cover\",\"hideCover\",true,\"tags\",[66,67,68,69,70,71,72],\"atUri\",\"at://did:plc:zntngpowgd6rorjt3haywj36/site.standard.document/3mooywkcs5s22\",\"appimage\",\"appherder\",\"linux\",\"systemd\",\"go\",\"cli\",\"package-management\"]\n");</script>
84<!--$-->
84<script>window.__reactRouterContext.streamController.close();</script>
84<!--/$--><!--/$-->
84<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"f3c3a7e5ac514ac1892ba6adb53076e1","r":1,"spa":2}' crossorigin="anonymous"></script>
84
85</body></html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.