<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>daal.cloud - blog</title>
  <subtitle>Vince van Daal&#39;s retro portal - client-side tools and a personal blog.</subtitle>
  <link href="https://daal.cloud/blog/feed.xml" rel="self"/>
  <link href="https://daal.cloud/blog/"/>
  <id>https://daal.cloud/blog/</id>
  
  <updated>2026-05-14T00:00:00.000Z</updated>
  
  <author>
    <name>Vince van Daal</name>
    <uri>https://vandaal.io</uri>
  </author>
  
  <entry>
    <title>My favourite anime</title>
    <link href="https://daal.cloud/blog/favourite-anime/"/>
    <id>https://daal.cloud/blog/favourite-anime/</id>
    <published>2026-05-14T00:00:00.000Z</published>
    <updated>2026-05-14T00:00:00.000Z</updated>
    <summary>A short tour through three anime I keep coming back to (Elfen Lied, Hellsing Ultimate, and Panty &amp; Stocking with Garterbelt), plus a quick aside on why subtitles always win.</summary>
    <content type="html">&lt;p&gt;If there&#39;s one thing I love, it&#39;s watching television. Or maybe it&#39;s just any screen, as I&#39;m mostly sitting in front of screens. Makes me suddenly remember how I fucked up my eyes by programming when I was about 17, a whole summer holiday long, on a good ol&#39; CRT monitor in a dimly-lit room. But I&#39;m getting off track :-).&lt;/p&gt;
&lt;p&gt;I wouldn&#39;t consider myself a huge anime fan. I used to have a friend who learned basic Japanese by basically inhaling a ton of anime shows, and that&#39;s a level of dedication I never got close to. But I&#39;ve definitely watched my fair share, and I have clear favourites. I always prefer them in Japanese with subtitles. The dubs are usually fine, but the original voice acting just lands differently for me. The cadence, the yelling, the awkward pauses. Dubs flatten that out, and on top of it the lip-sync compromises end up changing the lines themselves. So subtitles it is.&lt;/p&gt;
&lt;h2 id=&quot;elfen-lied&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#elfen-lied&quot;&gt;#&lt;/a&gt; Elfen Lied&lt;/h2&gt;
&lt;p&gt;Oh wonderful Elfen Lied. I can already hear &amp;quot;Lilium&amp;quot; popping up in my head as I just write down the title of the show. Elfen Lied still, to this date, just &amp;quot;hits&amp;quot; me.&lt;/p&gt;
&lt;p&gt;The character development is what does it. Lucy is the one we&#39;re talking about, although she also goes by Kaede, and by the gentler alter-ego Nyu. She&#39;s a Diclonius, a kind of next-step human with little horns and a set of invisible &amp;quot;vector&amp;quot; arms she can kill with from across a room. The show spends most of its runtime quietly explaining how she ended up that way.&lt;/p&gt;
&lt;details class=&quot;spoiler&quot;&gt;
&lt;summary&gt;The Kota bit (spoilers, click to reveal)&lt;/summary&gt;
&lt;p&gt;The relationship with Kota is the load-bearing piece. They met as kids, before any of the violence, when she was just a lonely orphanage girl and he was a boy on his summer holiday. He was kind to her. That early bond is basically the only piece of her life that wasn&#39;t cruelty or experimentation, and the rest of the show is asking what happens when you take a child, isolate her, abuse her, and then years later let her stumble back into that one thread of kindness. Honestly, chef&#39;s kiss.&lt;/p&gt;
&lt;/details&gt;
&lt;h2 id=&quot;hellsing-ultimate&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#hellsing-ultimate&quot;&gt;#&lt;/a&gt; Hellsing Ultimate&lt;/h2&gt;
&lt;p&gt;Then we have Hellsing. Hellsing Ultimate specifically. That&#39;s the 10-episode OVA, not the older 2001 TV series, which goes off-script halfway through and turns into a completely different show. (Although honestly the fan-made &lt;a href=&quot;https://en.wikipedia.org/wiki/Hellsing_Ultimate_Abridged&quot;&gt;Hellsing Ultimate Abridged&lt;/a&gt; is also a SOLID watch. Story-wise it&#39;s amazing, and I still laugh out loud every time I rewatch it.)&lt;/p&gt;
&lt;p&gt;What I like about Ultimate is that it commits. Alucard is an ancient vampire bound to serve the Hellsing Organisation, which hunts other vampires in the name of the British crown. Sir Integra Hellsing is the one holding his leash, and somehow she actually pulls it off. A young woman in a suit, ordering the most dangerous creature on the planet around, and you buy it. Then there&#39;s Seras Victoria, a former cop turned vampire under Alucard, and her arc of slowly accepting what she&#39;s become is honestly the most grounded part of the whole thing.&lt;/p&gt;
&lt;details class=&quot;spoiler&quot;&gt;
&lt;summary&gt;What the plot actually does (spoilers, click to reveal)&lt;/summary&gt;
&lt;p&gt;The Millennium plot is the engine. A leftover Nazi battalion of artificial vampires turns up to settle a 50-year-old grudge, the Vatican&#39;s Iscariot organisation gets dragged in, and London ends up flattened. The fights are absurd and over-the-top in a way only animation gets away with, and the script takes itself seriously enough that the absurdity lands instead of curdling. The whole thing reads like a long argument about what monstrosity is, and whether men or monsters do more damage. It doesn&#39;t really land on an answer, which I think is the point.&lt;/p&gt;
&lt;/details&gt;
&lt;h2 id=&quot;panty-stocking-with-garterbelt&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#panty-stocking-with-garterbelt&quot;&gt;#&lt;/a&gt; Panty &amp;amp; Stocking with Garterbelt&lt;/h2&gt;
&lt;p&gt;And now, in the voice of John Cleese to camera (possibly sitting at a desk in the middle of a field): &lt;em&gt;&amp;quot;And now for something completely different.&amp;quot;&lt;/em&gt; &lt;a href=&quot;https://en.wikipedia.org/wiki/Panty_%26_Stocking_with_Garterbelt&quot;&gt;Panty &amp;amp; Stocking with Garterbelt&lt;/a&gt;. Rowdy innuendos? Check. Song tracks that contain too much moaning (looking at you &amp;quot;Pantscada&amp;quot;, the whole OST was produced by &lt;a href=&quot;https://en.wikipedia.org/wiki/Teddyloid&quot;&gt;Teddyloid&lt;/a&gt; and featured all over the series)? Check.&lt;/p&gt;
&lt;p&gt;But mostly it has a ton of comedy that just hits my spot. It&#39;s two angel sisters kicked out of Heaven and parked in a place called Daten City, hunting ghosts for &amp;quot;Heaven coins&amp;quot; so they can earn their way back. The animation style is a deliberate slap at &amp;quot;tasteful&amp;quot; anime, closer to early Cartoon Network than to anything Studio Ghibli would put out, and they parody half the genre while they&#39;re at it. There&#39;s a transformation sequence in there that&#39;s basically Sailor Moon by way of a strip club.&lt;/p&gt;
&lt;p&gt;The OST is the other reason to watch. Teddyloid&#39;s work on this show is unreasonably good, and it shows up at exactly the right moments. &amp;quot;Fly Away&amp;quot; over a car-chase episode is the kind of track you queue up later to listen to on its own.&lt;/p&gt;
&lt;p&gt;And there is a twist at the end. I won&#39;t spoil it outright, but here&#39;s a small hint if you want one:&lt;/p&gt;
&lt;details class=&quot;spoiler&quot;&gt;
&lt;summary&gt;How the ending lands (mild hint, click to reveal)&lt;/summary&gt;
&lt;p&gt;The final episode commits to a bit so hard it almost stops being funny, and the long-awaited sequel that finally got greenlit is doing nothing to make things easier on people who liked clean endings.&lt;/p&gt;
&lt;/details&gt;
&lt;h2 id=&quot;so-why-anime&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#so-why-anime&quot;&gt;#&lt;/a&gt; So why anime?&lt;/h2&gt;
&lt;p&gt;Makes me think, why do I like anime? Maybe because I grew up with cartoons and watched too much Ren &amp;amp; Stimpy :-). The three shows above don&#39;t have a lot in common other than being animated and being willing to commit to a tone. But I guess it boils down to me watching them at certain periods in my life, when they really resonated. These days I barely watch any anime anymore... maybe it&#39;s time to start again?&lt;/p&gt;
</content>
    <category term="anime"/><category term="opinion"/><category term="tv"/>
  </entry>
  
  <entry>
    <title>Recipe: Easy Tray Bake</title>
    <link href="https://daal.cloud/blog/easy-tray-bake/"/>
    <id>https://daal.cloud/blog/easy-tray-bake/</id>
    <published>2026-05-14T00:00:00.000Z</published>
    <updated>2026-05-14T00:00:00.000Z</updated>
    <summary>A no-stress oven tray bake with AH meatballs, krieltjes, winter carrots and red onions. First food recipe on the blog. Comes with a slider to scale ingredients up for more people. Dutch translation available via the toggle at the top.</summary>
    <content type="html">&lt;p&gt;First food recipe on the blog. Wooooh! Time to see if Eleventy can handle a kitchen.&lt;/p&gt;
&lt;p&gt;This is one of those toss-everything-on-the-tray recipes. About ten minutes of &amp;quot;work&amp;quot; and thirty minutes of oven. Most of the active time is fighting with baking paper on your tray.&lt;/p&gt;
&lt;div class=&quot;recipe-scale&quot; data-recipe-scale data-base-servings=&quot;2&quot;&gt;
  &lt;div class=&quot;recipe-scale-label&quot;&gt;
    &lt;span&gt;Servings:&lt;/span&gt;
    &lt;button type=&quot;button&quot; class=&quot;recipe-scale-btn&quot; data-scale-minus aria-label=&quot;Decrease servings&quot;&gt;-&lt;/button&gt;
    &lt;input type=&quot;number&quot; data-scale-input min=&quot;1&quot; max=&quot;50&quot; value=&quot;2&quot; inputmode=&quot;numeric&quot; aria-label=&quot;Servings&quot;&gt;
    &lt;button type=&quot;button&quot; class=&quot;recipe-scale-btn&quot; data-scale-plus aria-label=&quot;Increase servings&quot;&gt;+&lt;/button&gt;
  &lt;/div&gt;
  &lt;p class=&quot;recipe-scale-note&quot;&gt;The base recipe serves &lt;strong&gt;2&lt;/strong&gt;. Scaled amounts are an estimate, adjust to taste. Whole items (onions, meatballs) round up.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;ingredients&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#ingredients&quot;&gt;#&lt;/a&gt; Ingredients&lt;/h2&gt;
&lt;ul class=&quot;recipe-ingredients&quot;&gt;
&lt;li&gt;&lt;span class=&quot;qty&quot; data-qty=&quot;1&quot;&gt;1&lt;/span&gt;x pack of AH Biologisch Rundergehakt balletjes (&lt;a href=&quot;https://www.ah.nl/producten/product/wi436877&quot;&gt;ah.nl&lt;/a&gt;) - that&#39;s &lt;span class=&quot;qty&quot; data-qty=&quot;12&quot;&gt;12&lt;/span&gt; small beef meatballs&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;qty&quot; data-qty=&quot;500&quot; data-unit=&quot;gram&quot;&gt;500 gram&lt;/span&gt; AH Krieltjes mix (&lt;a href=&quot;https://www.ah.nl/producten/product/wi482839&quot;&gt;ah.nl&lt;/a&gt;) - small waxy potatoes, mixed colours&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;qty&quot; data-qty=&quot;375&quot; data-unit=&quot;gram&quot;&gt;375 gram&lt;/span&gt; AH Biologisch Winterpeen (&lt;a href=&quot;https://www.ah.nl/producten/product/wi561136&quot;&gt;ah.nl&lt;/a&gt;) - winter carrots, about half a 750 gram pack&lt;/li&gt;
&lt;li&gt;&lt;span class=&quot;qty&quot; data-qty=&quot;2&quot; data-round=&quot;up&quot;&gt;2&lt;/span&gt; large red onions, or &lt;span class=&quot;qty&quot; data-qty=&quot;3&quot; data-round=&quot;up&quot;&gt;3&lt;/span&gt; small ones&lt;/li&gt;
&lt;li&gt;A bit of salt or Herbamare&lt;/li&gt;
&lt;li&gt;A bit of olive oil (engine oil is not recommended)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;time&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#time&quot;&gt;#&lt;/a&gt; Time&lt;/h2&gt;
&lt;p&gt;Prep depends on how good your knife skills are. Mine are mediocre, so I spend roughly 15 to 20 minutes getting everything ready. It tends to help if your partner, your mother, or your neighbour gets a head start on the potatoes. Something something efficiency. Then 30 minutes in the oven.&lt;/p&gt;
&lt;p&gt;Pro tip: switch the oven on to preheat &lt;strong&gt;right at the start&lt;/strong&gt; of your prep, so it&#39;s at temperature by the time the tray is ready. Use &lt;strong&gt;conventional top + bottom heat&lt;/strong&gt;, not the fan / hot-air setting.&lt;/p&gt;
&lt;h2 id=&quot;tools&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#tools&quot;&gt;#&lt;/a&gt; Tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Knife&lt;/li&gt;
&lt;li&gt;Cutting board&lt;/li&gt;
&lt;li&gt;Peeler (&amp;quot;dunschiller&amp;quot; in Dutch; &amp;quot;rasp&amp;quot; is the Dutch word for grater, and the linked &lt;a href=&quot;https://youtu.be/D_iqN7FGtIg&quot;&gt;video&lt;/a&gt; is a small Dutch meme of someone hilariously struggling to pronounce it)&lt;/li&gt;
&lt;li&gt;Oven&lt;/li&gt;
&lt;li&gt;Baking tray&lt;/li&gt;
&lt;li&gt;Baking paper&lt;/li&gt;
&lt;li&gt;Electricity&lt;/li&gt;
&lt;li&gt;Oven gloves, or a tea towel folded double&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We have a 90 cm oven and tray. A smaller one is fine too, you&#39;ll just probably need to split it across 2 trays or 2 rounds.&lt;/p&gt;
&lt;h2 id=&quot;instructions&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#instructions&quot;&gt;#&lt;/a&gt; Instructions&lt;/h2&gt;
&lt;p&gt;Grab the baking tray and chuck the baking paper on top. Once you have won the fight with the baking paper and it actually stays put (seriously, always a hassle, my tip is to wet it a bit), start by de-sprouting and lightly cleaning the potatoes. We&#39;re fans of potatoes with the skin on, because hey, why bother buying the pre-mixed bag otherwise ;-). But you do you! Cut them into bite-sized chunks and lay them on the baking tray on top of the baking paper (under it would be a bit strange).&lt;/p&gt;
&lt;p&gt;Peel the carrots with the peeler first (long strokes, away from your fingers, you know the drill). Then cut them into bite-sized chunks and put those on your baking tray too (yes, on top of the baking paper that is on top of the tray, you&#39;ve got the idea by now right? ;&#39;).&lt;/p&gt;
&lt;p&gt;Slice onion rings. Yes I know, boring work, but you can do this. I can do this, trust me, you can do this too. Drop them on the tray.&lt;/p&gt;
&lt;p&gt;Open the pack of meatballs. Try not to send the balls flying across the kitchen because you opened it a bit too enthusiastically (speaking from experience). Place them on the tray.&lt;/p&gt;
&lt;p&gt;Grab your olive oil (so NOT engine oil) and sprinkle a bit over everything on the tray. Or, if you want a less greasy result, grab a small bowl and a fancy silicone basting brush and brush everything with oil (hopefully you know how that works? Right?!?!? But if not: fill the bowl with some oil, dip the brush in the oil, brush the brush over the ingredients, and repeat until everything has a light coat).&lt;/p&gt;
&lt;p&gt;Now flick a bit of salt or Herbamare over them (the ingredients, you know, the ones on the baking paper on the baking tray).&lt;/p&gt;
&lt;p&gt;Put it (gently) in your &lt;em&gt;preheated&lt;/em&gt; oven at 200 degrees Celsius for about 30 minutes. Don&#39;t forget to set a timer.&lt;/p&gt;
&lt;p&gt;After 30 minutes, when your oven goes BEEP BEEP, or whatever your timer does, put on your oven gloves, or one of those fancy thermal suits you see people wearing near volcanoes, or just grab a tea towel and fold it double, or burn your hands (not recommended). Take the food out of the oven (yes yes, the whole shebang, tray and all, just dump the lot onto your worktop) and spoon it onto a plate.&lt;/p&gt;
&lt;p&gt;Or spoon it onto multiple plates, or eat it straight off the scorching hot tray. Whatever floats your boat. I&#39;d grab a plate.&lt;/p&gt;
&lt;p&gt;Voila! Bon appétit!&lt;/p&gt;
</content>
    <category term="recipes"/><category term="food"/><category term="tray-bake"/><category term="nederlands"/>
  </entry>
  
  <entry>
    <title>Co-writing with AI, without the slop</title>
    <link href="https://daal.cloud/blog/co-writing-with-ai/"/>
    <id>https://daal.cloud/blog/co-writing-with-ai/</id>
    <published>2026-05-13T00:00:00.000Z</published>
    <updated>2026-05-13T00:00:00.000Z</updated>
    <summary>How I draft blog posts with Claude as a co-writer. Markers, /tagore, and a human pass at the end. The &quot;ghostwriter&quot; framing doesn&#39;t quite fit. &quot;Co-writer&quot; does.</summary>
    <content type="html">&lt;p&gt;I use AI to help write this blog. Shocking, I know. It&#39;s 2026. My fridge probably uses AI to schedule its own defrost cycle, the toaster has a personality, and at this point if a blog post wasn&#39;t at least partly drafted by a language model, that&#39;s the unusual case.&lt;/p&gt;
&lt;h2 id=&quot;how-i-do-it&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-i-do-it&quot;&gt;#&lt;/a&gt; How I do it&lt;/h2&gt;
&lt;p&gt;I usually write an excerpt or the rough idea myself, drop it into a prompt with markers like &lt;code&gt;(Claude note: expand on this with X or Y)&lt;/code&gt;, run &lt;a href=&quot;https://github.com/apurvrdx1/tagore&quot;&gt;&lt;code&gt;/tagore&lt;/code&gt;&lt;/a&gt; over the result, and then go back through it myself because tagore doesn&#39;t catch everything and I have opinions about how a sentence should land. I check every post myself, Vince the human ;-), before it ships.&lt;/p&gt;
&lt;p&gt;That&#39;s it. That&#39;s the whole workflow.&lt;/p&gt;
&lt;h2 id=&quot;is-that-shocking-does-it-kill-the-human-element&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#is-that-shocking-does-it-kill-the-human-element&quot;&gt;#&lt;/a&gt; Is that shocking? Does it kill the human element?&lt;/h2&gt;
&lt;p&gt;I don&#39;t think so.&lt;/p&gt;
&lt;p&gt;The part I write first is the load-bearing piece: the hot take, the structure, the specific examples, what I want to say. The model expands it, fills in transitions, suggests phrasing I wouldn&#39;t have reached for, and then I edit the parts that landed wrong. The opinions are mine. The choice to publish is mine.&lt;/p&gt;
&lt;p&gt;&amp;quot;Proofreader&amp;quot; undersells it. Proofreading is comma placement and dangling modifiers, and Claude does a lot more than that. It offers ways to phrase a paragraph that I&#39;d never have written but, once I read it, I recognise as the thing I was trying to say. &amp;quot;Ghostwriter&amp;quot; overshoots. Ghostwriters write the whole thing while the named author signs off. Neither fits.&lt;/p&gt;
&lt;p&gt;Co-writer is the word. Funny timing: Anthropic recently launched &lt;a href=&quot;https://www.anthropic.com/product/claude-cowork&quot;&gt;Claude Cowork&lt;/a&gt;, which is roughly the same framing: Claude as someone you collaborate with on the actual work, not as an oracle you query. Different product, same idea. Stop pretending the AI is a vending machine and start treating it like the smart colleague who&#39;s read everything and types fast.&lt;/p&gt;
&lt;h2 id=&quot;the-hot-take&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-hot-take&quot;&gt;#&lt;/a&gt; The hot take&lt;/h2&gt;
&lt;p&gt;The &amp;quot;AI vs human writing&amp;quot; frame is the wrong one in 2026. The interesting question is whether anyone thought about the writing before publishing. Whether a human typed it or a model drafted it matters much less.&lt;/p&gt;
&lt;p&gt;A model can produce a thousand competent-but-soulless posts a day. A human can also produce a thousand competent-but-soulless posts a day. Look at any content marketing pipeline from 2018. Neither is interesting.&lt;/p&gt;
&lt;p&gt;What I notice when I read good writing now is that the author had a point of view, picked the specific examples that matter, and trusted the reader. None of that depends on whether a human or a model wrote the first draft. Pure-human writing with no point of view is just as boring as pure-AI writing with no point of view. The collaboration model, where the human supplies opinion and judgment and the model supplies fluency, produces better stuff than either does alone, provided you do the judgment part instead of mass-accepting the suggestions.&lt;/p&gt;
&lt;p&gt;So no, AI didn&#39;t take the human out of writing. It took the typing out. Which, honestly, was always the boring part.&lt;/p&gt;
&lt;h2 id=&quot;the-practical-upside&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-practical-upside&quot;&gt;#&lt;/a&gt; The practical upside&lt;/h2&gt;
&lt;p&gt;This setup lets me publish faster, and from places where I&#39;d never normally bother to draft. A post like this one can start as a phone-typed lump of thoughts on the train, get expanded against my usual workflow, run through tagore, and end up reviewable by the time I&#39;m home. That used to be a half-day&#39;s work at a laptop. Now it&#39;s an evening with the laptop just for the edit pass.&lt;/p&gt;
&lt;p&gt;The blog you&#39;re reading exists because the friction of writing a post dropped enough that I do it at all. That, on its own, would be enough to justify the workflow. Everything else is upside.&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/using-ai-for-coding/&quot;&gt;My take on using AI for coding&lt;/a&gt;. The same thinking, applied to code instead of prose.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apurvrdx1/tagore&quot;&gt;&lt;code&gt;/tagore&lt;/code&gt; on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="claude"/><category term="ai"/><category term="writing"/><category term="blog-workflow"/><category term="opinion"/>
  </entry>
  
  <entry>
    <title>My take on using AI for coding</title>
    <link href="https://daal.cloud/blog/using-ai-for-coding/"/>
    <id>https://daal.cloud/blog/using-ai-for-coding/</id>
    <published>2026-05-07T00:00:00.000Z</published>
    <updated>2026-05-07T00:00:00.000Z</updated>
    <summary>Where I land on AI-assisted coding in 2026. Claude Code and the rest are powerful, but prompt steering, guardrails, spec-driven dev and the RALPH loop are what separate &quot;this works&quot; from &quot;I have no idea what I shipped&quot;.</summary>
    <content type="html">&lt;h2 id=&quot;what-i-actually-use&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what-i-actually-use&quot;&gt;#&lt;/a&gt; What I actually use&lt;/h2&gt;
&lt;p&gt;Mostly &lt;a href=&quot;https://claude.ai/&quot;&gt;Claude&lt;/a&gt;. Claude Code in the terminal, Claude in the browser for one-off thinking. I&#39;ve tried &lt;a href=&quot;https://www.cursor.com/&quot;&gt;Cursor&lt;/a&gt; and &lt;a href=&quot;https://github.com/features/copilot&quot;&gt;GitHub Copilot&lt;/a&gt; too, and &lt;a href=&quot;https://openai.com/codex/&quot;&gt;Codex&lt;/a&gt; is competitive on a lot of tasks. Honestly the gap between the top tools is small compared to the gap between &amp;quot;good prompt with guardrails&amp;quot; and &amp;quot;vibe-coded one-shot&amp;quot;. The model matters less than I expected. The harness around it matters more.&lt;/p&gt;
&lt;h2 id=&quot;the-bottleneck-moved&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-bottleneck-moved&quot;&gt;#&lt;/a&gt; The bottleneck moved&lt;/h2&gt;
&lt;p&gt;When I wrote everything by hand, the slow step was typing and remembering syntax. Now the slow step is reviewing what the model produced and making sure it actually does what I asked, on the inputs I care about, without quietly breaking something else.&lt;/p&gt;
&lt;p&gt;So the work shifted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Less time writing boilerplate, glue code, first-pass implementations.&lt;/li&gt;
&lt;li&gt;More time reading diffs, writing tests for the diffs, and asking &amp;quot;wait, why did it touch that file?&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn&#39;t a complaint. Reading and reviewing is higher-leverage than typing. But anyone who claims AI coding is &amp;quot;10x faster&amp;quot; without mentioning that the review burden went up too is selling something.&lt;/p&gt;
&lt;h2 id=&quot;prompt-steering-and-guardrails-do-most-of-the-work&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#prompt-steering-and-guardrails-do-most-of-the-work&quot;&gt;#&lt;/a&gt; Prompt steering and guardrails do most of the work&lt;/h2&gt;
&lt;p&gt;The biggest jump in output quality I&#39;ve seen comes from constraining the model up front:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;CLAUDE.md&lt;/code&gt; (or equivalent) at the repo root that spells out conventions, commands, and the shape of the codebase. Claude Code reads it on every session.&lt;/li&gt;
&lt;li&gt;Permissions, hooks, and explicit allow-lists so the agent can&#39;t run arbitrary destructive commands without asking.&lt;/li&gt;
&lt;li&gt;Sub-agents and skills for repeatable workflows. The post you&#39;re reading was created by a private skill of mine that enforces &amp;quot;branch from origin/main, run /tagore, open a PR, never merge&amp;quot;. It&#39;s not really a tool, just a workflow I trust wrapped in something Claude can invoke.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without those rails you end up with a very smart intern who occasionally &lt;code&gt;rm -rf&lt;/code&gt;s something they shouldn&#39;t. With them you get a very smart intern who actually checks before doing anything load-bearing.&lt;/p&gt;
&lt;h2 id=&quot;hey-claude-design-a-full-blown-fullstack-knowledge-base-make-no-mistakes&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#hey-claude-design-a-full-blown-fullstack-knowledge-base-make-no-mistakes&quot;&gt;#&lt;/a&gt; &amp;quot;Hey Claude, design a full-blown fullstack knowledge base. Make no mistakes.&amp;quot;&lt;/h2&gt;
&lt;p&gt;That kind of prompt almost never produces what you actually want. It produces &lt;em&gt;something&lt;/em&gt;, usually a plausible-looking scaffold, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The model picks defaults that don&#39;t match your stack, your hosting, or your constraints.&lt;/li&gt;
&lt;li&gt;Trade-offs you&#39;d have argued about (auth model, data store, deploy target) get resolved silently.&lt;/li&gt;
&lt;li&gt;Six iterations later you&#39;ve got a folder full of code you didn&#39;t choose, and when something breaks you have no map of what depends on what.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&#39;ve watched this play out on side projects and on bigger work too. The further you let the AI run without you actually understanding the architecture, the more painful the eventual debugging session. Because at some point there &lt;em&gt;will&lt;/em&gt; be a debugging session, and the only person who can drive it is someone who understands how the pieces fit. If that&#39;s not you, you&#39;re stuck.&lt;/p&gt;
&lt;p&gt;So the takeaway isn&#39;t &amp;quot;don&#39;t use AI for new projects&amp;quot;. It&#39;s that understanding the architecture of your application matters more, not less, when AI writes most of the code. You don&#39;t need to type every line, but you do need to be able to redraw the box-and-arrow diagram from memory.&lt;/p&gt;
&lt;h2 id=&quot;what-works-better-specs-and-ralph&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what-works-better-specs-and-ralph&quot;&gt;#&lt;/a&gt; What works better: specs and RALPH&lt;/h2&gt;
&lt;p&gt;The pattern I keep coming back to is spec-driven development plus a tight loop.&lt;/p&gt;
&lt;p&gt;Spec-driven means: before the model writes code, write down (or have it write down, then edit) what the thing should do, what it should not do, what the inputs and outputs are, and where the edges are. Save that spec next to the code. Then prompt against the spec, not against vibes.&lt;/p&gt;
&lt;p&gt;The loop part is what &lt;a href=&quot;https://awesomeclaude.ai/ralph-wiggum&quot;&gt;Geoffrey Huntley calls RALPH&lt;/a&gt;, short for Read, Analyse, Loop, Plan, Hone. The short version: don&#39;t ask for the whole feature in one shot. Ask the model to read the spec, look at the current state, plan the next small change, make it, then loop. Each pass is small enough to review honestly. Each pass updates the spec and the code together.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/blader/claude-mem&quot;&gt;claude-mem&lt;/a&gt; bakes a similar workflow in directly. &lt;code&gt;/make-plan&lt;/code&gt; produces a phased spec, &lt;code&gt;/do&lt;/code&gt; executes it pass by pass with subagents, and observations from each session feed the next one. One caveat: &lt;code&gt;/make-plan&lt;/code&gt; and &lt;code&gt;/do&lt;/code&gt; are barely documented. You kind of have to read the skill source to understand what they actually do. But once you do, they work brilliantly in my experience. There are alternatives, &lt;a href=&quot;https://github.com/gsd-build/get-shit-done&quot;&gt;get-shit-done&lt;/a&gt; being one of the more visible ones, but most of them feel bloated to me. claude-mem stays small and gets out of the way, which is what I want.&lt;/p&gt;
&lt;h2 id=&quot;what-i-d-tell-someone-starting-today&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#what-i-d-tell-someone-starting-today&quot;&gt;#&lt;/a&gt; What I&#39;d tell someone starting today&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Pick one tool (Claude Code, Cursor, whichever) and learn its harness deeply. Switching constantly is a tax.&lt;/li&gt;
&lt;li&gt;Always have a &lt;code&gt;CLAUDE.md&lt;/code&gt; (or equivalent) at the repo root. Update it when conventions change.&lt;/li&gt;
&lt;li&gt;Write the spec first. Even three bullet points beats nothing.&lt;/li&gt;
&lt;li&gt;Loop in small increments. Review every diff. Don&#39;t merge what you can&#39;t explain.&lt;/li&gt;
&lt;li&gt;When the model surprises you, good or bad, figure out &lt;em&gt;why&lt;/em&gt; before continuing.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The tools are good. They&#39;re not magic, and they don&#39;t replace the work of knowing what you&#39;re building. The people I see getting the most out of them treat the AI as a fast collaborator that needs a clear spec and tight feedback, not as an oracle.&lt;/p&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/overview&quot;&gt;Claude Code docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://awesomeclaude.ai/ralph-wiggum&quot;&gt;RALPH method on awesomeclaude.ai&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/blader/claude-mem&quot;&gt;claude-mem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/gsd-build/get-shit-done&quot;&gt;get-shit-done&lt;/a&gt; for comparison&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="claude"/><category term="ai"/><category term="claude-code"/><category term="spec-driven-development"/><category term="ralph"/><category term="opinion"/>
  </entry>
  
  <entry>
    <title>Fixing Chromium virtual backgrounds on NVIDIA + Wayland</title>
    <link href="https://daal.cloud/blog/chromium-virtual-background-nvidia-wayland/"/>
    <id>https://daal.cloud/blog/chromium-virtual-background-nvidia-wayland/</id>
    <published>2026-05-04T00:00:00.000Z</published>
    <updated>2026-05-04T00:00:00.000Z</updated>
    <summary>Meet&#39;s background blur turns your video into a solid white rectangle on NVIDIA + Wayland Chromium. The cause is a Chromium ↔ NVIDIA EGL interop bug in the canvas captureStream path. Two flags work around it; pick based on monitor setup.</summary>
    <content type="html">&lt;p&gt;Switched on background blur in a Meet call and turned into a flat white rectangle. Camera works fine without the effect. The moment any background filter (blur or virtual background) is applied, everyone on the call (me in the self-preview included) sees a solid white frame where my video should be. Reproduces in any Chromium-based browser (Chromium, Vivaldi). Firefox is fine. Setup that hits this: NVIDIA proprietary driver (tested on 595.71.05, open kernel module branch, RTX 4090), KDE Plasma 6 on Wayland (&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&lt;/a&gt;), Flatpak Chromium 147.&lt;/p&gt;
&lt;h2 id=&quot;why-it-breaks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-it-breaks&quot;&gt;#&lt;/a&gt; Why it breaks&lt;/h2&gt;
&lt;p&gt;Meet&#39;s virtual background pipeline runs per frame:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;camera frame
  → MediaPipe TFLite WebGL segmentation
  → WebGL composite shader (frame + background, masked)
  → canvas.captureStream() → MediaStream
  → WebRTC encoder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;captureStream()&lt;/code&gt; hop has to share the WebGL output texture cross-process to the GPU process where the encoder runs. Chromium does this through its &lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/HEAD/docs/gpu/shared_image.md&quot;&gt;SharedImage&lt;/a&gt; abstraction, which on Linux maps to EGLImage / dmabuf. On NVIDIA&#39;s Wayland EGL implementation, the shared image arrives at the encoder side either uninitialised or wrongly synchronised, so reads come back as all-1s. Solid white frames.&lt;/p&gt;
&lt;p&gt;The shader runs and the composite is correct. The readback path is the broken thing.&lt;/p&gt;
&lt;p&gt;Firefox doesn&#39;t hit this because it doesn&#39;t use ANGLE or Chromium&#39;s SharedImage. Its &lt;code&gt;captureStream&lt;/code&gt; goes through a different path (often a CPU readback) that never exercises the broken NVIDIA EGL interop.&lt;/p&gt;
&lt;h2 id=&quot;fix&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#fix&quot;&gt;#&lt;/a&gt; Fix&lt;/h2&gt;
&lt;p&gt;Two flags work. Pick one based on monitor setup.&lt;/p&gt;
&lt;h3 id=&quot;option-1-ozone-platform-x11-preferred-on-a-desktop-with-same-scale-monitors&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#option-1-ozone-platform-x11-preferred-on-a-desktop-with-same-scale-monitors&quot;&gt;#&lt;/a&gt; Option 1: &lt;code&gt;--ozone-platform=x11&lt;/code&gt; (preferred on a desktop with same-scale monitors)&lt;/h3&gt;
&lt;p&gt;Runs Chromium under XWayland. GPU compositing stays on, WebGL/canvas/video decode stay full-speed, PipeWire screen sharing still works (it&#39;s independent of Ozone). Cost:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;XWayland is integer-scale only. No fractional HiDPI, no proper per-monitor scaling.&lt;/li&gt;
&lt;li&gt;Drag-and-drop and clipboard with native Wayland apps can be slightly flaky.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fine on a desktop with one or two monitors at the same scale. Annoying if you mix a 4K-at-200% panel with a 1080p-at-100% one.&lt;/p&gt;
&lt;h3 id=&quot;option-2-disable-gpu-compositing-preferred-on-laptops-or-mixed-scale-monitors&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#option-2-disable-gpu-compositing-preferred-on-laptops-or-mixed-scale-monitors&quot;&gt;#&lt;/a&gt; Option 2: &lt;code&gt;--disable-gpu-compositing&lt;/code&gt; (preferred on laptops or mixed-scale monitors)&lt;/h3&gt;
&lt;p&gt;Wayland-native integration kept. Compositor runs on CPU. WebGL and canvas individual draws still hit the GPU; only the final composite is software. Cost:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Continuous extra CPU usage.&lt;/li&gt;
&lt;li&gt;Less smooth scrolling on heavy pages.&lt;/li&gt;
&lt;li&gt;Worse battery on laptops.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Genuinely unnoticeable on a 4090 desktop. Can matter on a laptop.&lt;/p&gt;
&lt;h3 id=&quot;how-to-apply-flatpak-chromium&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-to-apply-flatpak-chromium&quot;&gt;#&lt;/a&gt; How to apply (Flatpak Chromium)&lt;/h3&gt;
&lt;p&gt;Don&#39;t edit the system .desktop file. Flatpak rewrites it on every update. Copy it to &lt;code&gt;~/.local/share/applications/&lt;/code&gt; instead and edit the local copy. KDE picks up the local override automatically:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp /var/lib/flatpak/exports/share/applications/org.chromium.Chromium.desktop &#92;
   ~/.local/share/applications/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the chosen flag to all three &lt;code&gt;Exec=&lt;/code&gt; lines (main, &amp;quot;New Window&amp;quot; action, &amp;quot;New Incognito Window&amp;quot; action). For Option 1 the main line becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;Exec=/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=/app/bin/chromium --file-forwarding org.chromium.Chromium --ozone-platform=x11 @@u %U @@
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quit Chromium fully and relaunch. Verify in &lt;code&gt;chrome://gpu&lt;/code&gt;: &amp;quot;Ozone platform&amp;quot; should now read &lt;code&gt;x11&lt;/code&gt;, or under Option 2 &amp;quot;Compositing&amp;quot; should read software-only.&lt;/p&gt;
&lt;p&gt;For Vivaldi the equivalent lives in &lt;code&gt;~/.config/vivaldi-stable.conf&lt;/code&gt;. Same flags, see &lt;a href=&quot;/blog/vivaldi-persistent-flags/&quot;&gt;Persistent CLI flags for Vivaldi on Linux&lt;/a&gt; for the conf-file syntax.&lt;/p&gt;
&lt;h2 id=&quot;things-i-tried-that-didn-t-work&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#things-i-tried-that-didn-t-work&quot;&gt;#&lt;/a&gt; Things I tried that didn&#39;t work&lt;/h2&gt;
&lt;p&gt;For future-me, so I don&#39;t bisect them again. None of these fix it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--disable-features=WebRtcVideoCaptureGpuMemoryBuffer&lt;/code&gt;. Disables zero-copy GPU buffers for camera capture. The camera path is fine, the bug doesn&#39;t live here.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--disable-features=WebRtcVideoCaptureGpuMemoryBuffer,VaapiVideoDecoder&lt;/code&gt;. Adds VA-API decode disable on top. Same outcome.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--use-angle=gl&lt;/code&gt;. Forces ANGLE off Vulkan onto plain GL. The ANGLE backend isn&#39;t the broken thing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--disable-features=WebRTCPipeWireCamera&lt;/code&gt;. Falls back to V4L2 capture. Doesn&#39;t matter; camera path is fine.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--disable-features=Vulkan&lt;/code&gt;. Kills Vulkan everywhere. The shared-image bug doesn&#39;t go through Vulkan in this configuration.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--disable-accelerated-2d-canvas&lt;/code&gt;. Forces 2D canvas to CPU. MediaPipe outputs from a WebGL canvas, so this is the wrong canvas.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only the two flags above cross the broken path entirely (Ozone on X11 sidesteps the NVIDIA Wayland EGL path; &lt;code&gt;--disable-gpu-compositing&lt;/code&gt; forces a CPU readback that doesn&#39;t need shared images at all).&lt;/p&gt;
&lt;h2 id=&quot;the-real-fix-lives-upstream&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-real-fix-lives-upstream&quot;&gt;#&lt;/a&gt; The real fix lives upstream&lt;/h2&gt;
&lt;p&gt;This is a Chromium ↔ NVIDIA Wayland EGL interop bug. Almost certainly already tracked in &lt;a href=&quot;https://issues.chromium.org/&quot;&gt;issues.chromium.org&lt;/a&gt;. Worth searching with terms like &lt;em&gt;chromium wayland nvidia canvas captureStream white&lt;/em&gt; or &lt;em&gt;MediaPipe NVIDIA Wayland virtual background&lt;/em&gt; before filing anything new.&lt;/p&gt;
&lt;p&gt;Could also end up being a Flathub packaging fix. The Chromium flatpak already pre-applies &lt;code&gt;--enable-features=WebRTCPipeWireCapturer&lt;/code&gt; for exactly this kind of platform-specific compatibility patch, so a future build could ship one of these flags by default for NVIDIA users.&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/vivaldi-persistent-flags/&quot;&gt;Persistent CLI flags for Vivaldi on Linux&lt;/a&gt;. Same flags, applied via Vivaldi&#39;s conf-file mechanism.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;. Why the browser is a Flatpak in the first place.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/flatpak-1password-browser-bridge/&quot;&gt;Making 1Password browser extensions talk to the Flatpak desktop app&lt;/a&gt;. Another Flatpak browser quirk worth knowing about.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://peter.sh/experiments/chromium-command-line-switches/&quot;&gt;Chromium command-line switches&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/HEAD/docs/gpu/shared_image.md&quot;&gt;Chromium SharedImage design overview&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://chromium.googlesource.com/chromium/src/+/HEAD/docs/ozone_overview.md&quot;&gt;Ozone platform abstraction&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter&quot;&gt;MediaPipe selfie segmentation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="linux"/><category term="chromium"/><category term="nvidia"/><category term="wayland"/><category term="webrtc"/><category term="bazzite"/><category term="troubleshooting"/>
  </entry>
  
  <entry>
    <title>Making CoolerControl&#39;s GUI start on login</title>
    <link href="https://daal.cloud/blog/coolercontrol-autostart/"/>
    <id>https://daal.cloud/blog/coolercontrol-autostart/</id>
    <published>2026-05-03T00:00:00.000Z</published>
    <updated>2026-05-03T00:00:00.000Z</updated>
    <summary>CoolerControl ships a system daemon and a separate Qt GUI. The daemon controls fans at boot regardless of login, but the rpm doesn&#39;t install an autostart .desktop for the GUI, so the tray icon never appears. Drop one in `~/.config/autostart/` (or use Plasma&#39;s System Settings → Autostart) to fix it.</summary>
    <content type="html">&lt;p&gt;CoolerControl is two binaries: a system daemon (&lt;code&gt;coolercontrold&lt;/code&gt;) and a Qt GUI client (&lt;code&gt;coolercontrol&lt;/code&gt;). The daemon does the actual work (pump curves, fan curves, temperature monitoring), and the rpm enables it as a systemd service, so cooling stays correct at boot whether you log in or not. The GUI is just a viewer for the daemon&#39;s API plus the configuration UI.&lt;/p&gt;
&lt;p&gt;When you reboot and there&#39;s no tray icon, your fans are still being controlled. You&#39;re missing a UI, not a fan curve. But you&#39;ll still want it back.&lt;/p&gt;
&lt;p&gt;Notes below are written for KDE Plasma (the default Bazzite spin) on Wayland. GNOME-specific deltas are called out where they matter.&lt;/p&gt;
&lt;h2 id=&quot;why-it-doesn-t-autostart&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-it-doesn-t-autostart&quot;&gt;#&lt;/a&gt; Why it doesn&#39;t autostart&lt;/h2&gt;
&lt;p&gt;The Fedora rpm (&lt;code&gt;coolercontrol-4.2.0-1.fc44.x86_64&lt;/code&gt; at the time of writing) installs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/usr/lib/systemd/system/coolercontrold.service&lt;/code&gt;, the daemon, enabled by default.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/usr/share/applications/org.coolercontrol.CoolerControl.desktop&lt;/code&gt;, a launcher entry for the apps menu.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What&#39;s missing: no &lt;code&gt;/etc/xdg/autostart/coolercontrol.desktop&lt;/code&gt;, and the package doesn&#39;t drop anything into &lt;code&gt;~/.config/autostart/&lt;/code&gt; on first run either. The entry under &lt;code&gt;applications/&lt;/code&gt; puts CoolerControl in your apps menu. XDG autostart loaders don&#39;t look there.&lt;/p&gt;
&lt;p&gt;Different from &lt;a href=&quot;/blog/kopia-flatpak-autostart/&quot;&gt;Making KopiaUI start on login when installed as a Flatpak&lt;/a&gt;, where an in-app toggle exists but silently writes into a sandboxed config dir. Here there&#39;s no toggle that writes a .desktop file at all; the package just doesn&#39;t ship autostart.&lt;/p&gt;
&lt;h2 id=&quot;fix-plasma-click-path&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#fix-plasma-click-path&quot;&gt;#&lt;/a&gt; Fix (Plasma, click path)&lt;/h2&gt;
&lt;p&gt;If you&#39;d rather not edit dotfiles: &lt;em&gt;System Settings → Autostart → Add → Add Application → CoolerControl → OK&lt;/em&gt;. Plasma writes a &lt;code&gt;.desktop&lt;/code&gt; under &lt;code&gt;~/.config/autostart/&lt;/code&gt; for you and applies it on the next session start.&lt;/p&gt;
&lt;h2 id=&quot;fix-any-de-file-path&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#fix-any-de-file-path&quot;&gt;#&lt;/a&gt; Fix (any DE, file path)&lt;/h2&gt;
&lt;p&gt;Drop a minimal autostart entry on the host:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# ~/.config/autostart/coolercontrol.desktop
[Desktop Entry]
Type=Application
Name=CoolerControl
Comment=Monitor and control your cooling device
Exec=coolercontrol
Icon=org.coolercontrol.CoolerControl
Terminal=false
StartupNotify=false
X-GNOME-Autostart-enabled=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Plasma needs nothing more than a valid XDG &lt;code&gt;.desktop&lt;/code&gt; file in &lt;code&gt;~/.config/autostart/&lt;/code&gt;. Its session manager honours the freedesktop spec directly. The &lt;code&gt;X-GNOME-Autostart-enabled=true&lt;/code&gt; line is a GNOME-specific extension; Plasma ignores it, but leaving it in keeps the file portable if you ever switch DE or copy it to another machine.&lt;/p&gt;
&lt;p&gt;Verify after a reboot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pgrep -af coolercontrol
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see two processes: &lt;code&gt;coolercontrold&lt;/code&gt; under the system slice (the daemon) and &lt;code&gt;coolercontrol&lt;/code&gt; under your user session (the GUI).&lt;/p&gt;
&lt;h2 id=&quot;get-it-to-start-in-the-tray&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#get-it-to-start-in-the-tray&quot;&gt;#&lt;/a&gt; Get it to start in the tray&lt;/h2&gt;
&lt;p&gt;By default the GUI opens its main window on launch, which is annoying as an autostart. There&#39;s no &lt;code&gt;--start-in-tray&lt;/code&gt; CLI flag despite forum threads claiming there is. &lt;code&gt;coolercontrol --help&lt;/code&gt; lists &lt;code&gt;-h&lt;/code&gt;, &lt;code&gt;-v&lt;/code&gt;, &lt;code&gt;-d&lt;/code&gt;, &lt;code&gt;--full-debug&lt;/code&gt;, &lt;code&gt;--disable-gpu&lt;/code&gt;, &lt;code&gt;--chromium-flags&lt;/code&gt;, &lt;code&gt;--clear-cache&lt;/code&gt;. That&#39;s it.&lt;/p&gt;
&lt;p&gt;Tray-on-launch is a setting inside the app: &lt;em&gt;Settings → Start in Tray&lt;/em&gt;. Enable it once with the GUI open, then future autostarts come up minimised to the system tray.&lt;/p&gt;
&lt;p&gt;Plasma&#39;s tray shows the CoolerControl icon out of the box (StatusNotifierItem support is native). GNOME needs the &lt;strong&gt;AppIndicator and KStatusNotifierItem Support&lt;/strong&gt; extension. Without it, the GUI is running but invisible: no main window, no tray icon, just a process you can only see in &lt;code&gt;pgrep&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;when-it-still-doesn-t-work&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#when-it-still-doesn-t-work&quot;&gt;#&lt;/a&gt; When it still doesn&#39;t work&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Daemon isn&#39;t running.&lt;/strong&gt; &lt;code&gt;systemctl status coolercontrold.service&lt;/code&gt;. If it&#39;s disabled, &lt;code&gt;sudo systemctl enable --now coolercontrold.service&lt;/code&gt;. The GUI will sit at &amp;quot;Connecting to daemon&amp;quot; forever without it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GUI launches but pegs at &amp;quot;Connecting to daemon&amp;quot; with the daemon up.&lt;/strong&gt; The daemon listens on &lt;code&gt;127.0.0.1:11987&lt;/code&gt; over HTTPS with a self-signed cert. If you&#39;ve tightened the local firewall or replaced the cert, that&#39;s where to start.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wayland session, GUI window flashes on login then closes.&lt;/strong&gt; Check &lt;code&gt;journalctl --user -b | grep -i coolercontrol&lt;/code&gt;. Usually a Qt platform plugin or GPU acceleration issue. Add &lt;code&gt;--disable-gpu&lt;/code&gt; to the &lt;code&gt;Exec=&lt;/code&gt; line as a workaround.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plasma&#39;s &lt;em&gt;System Settings → Autostart&lt;/em&gt; lists the entry but it doesn&#39;t run.&lt;/strong&gt; Check perms on the desktop file. It must be readable by your user. &lt;code&gt;chmod 644 ~/.config/autostart/coolercontrol.desktop&lt;/code&gt;. Same applies to GNOME&#39;s &lt;em&gt;Tweaks → Startup Applications&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-not-just-the-system-desktop-file&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-not-just-the-system-desktop-file&quot;&gt;#&lt;/a&gt; Why not just the system desktop file&lt;/h2&gt;
&lt;p&gt;You might be tempted to copy &lt;code&gt;/usr/share/applications/org.coolercontrol.CoolerControl.desktop&lt;/code&gt; into &lt;code&gt;/etc/xdg/autostart/&lt;/code&gt;. Don&#39;t, on Bazzite or any other rpm-ostree atomic system: &lt;code&gt;/etc&lt;/code&gt; is layered, but &lt;code&gt;/usr&lt;/code&gt; is read-only and gets rebuilt on every rebase. A user-level autostart entry under &lt;code&gt;~/.config/autostart/&lt;/code&gt; survives both system updates and rebases, and doesn&#39;t need root.&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/kopia-flatpak-autostart/&quot;&gt;Making KopiaUI start on login when installed as a Flatpak&lt;/a&gt;. Same family of &amp;quot;Linux desktop autostart is annoying&amp;quot; problem, different root cause: Flatpak sandbox vs missing package autostart entry.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;. Why this matters more on immutable Fedora: &lt;code&gt;/usr/share/applications/&lt;/code&gt; is read-only, so you can&#39;t fix this by editing the system desktop file.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/flatpak-1password-browser-bridge/&quot;&gt;Making 1Password browser extensions talk to the Flatpak desktop app&lt;/a&gt;. Another sandbox/permissions wrinkle in the same family of desktop-Linux papercuts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.coolercontrol.org/&quot;&gt;CoolerControl documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html&quot;&gt;XDG Autostart specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gitlab.com/coolercontrol/coolercontrol&quot;&gt;CoolerControl GitLab repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="linux"/><category term="coolercontrol"/><category term="hardware"/><category term="bazzite"/><category term="kde"/><category term="plasma"/><category term="troubleshooting"/>
  </entry>
  
  <entry>
    <title>Making KopiaUI start on login when installed as a Flatpak</title>
    <link href="https://daal.cloud/blog/kopia-flatpak-autostart/"/>
    <id>https://daal.cloud/blog/kopia-flatpak-autostart/</id>
    <published>2026-04-28T00:00:00.000Z</published>
    <updated>2026-05-03T00:00:00.000Z</updated>
    <summary>KopiaUI&#39;s built-in &quot;Launch at startup&quot; toggle silently fails under Flatpak. The sandbox can&#39;t write to host `~/.config/autostart/`. Fix is a hand-rolled autostart `.desktop` file with a minimal `flatpak run` Exec line.</summary>
    <content type="html">&lt;p&gt;KopiaUI installed as a Flatpak (&lt;code&gt;io.kopia.KopiaUI&lt;/code&gt;) has a &amp;quot;Launch at startup&amp;quot; toggle in &lt;em&gt;Settings → Preferences&lt;/em&gt;. Under Flatpak, ticking it does nothing useful. The app reports success, but no autostart entry lands on the host and Kopia just doesn&#39;t come back after reboot.&lt;/p&gt;
&lt;p&gt;Sandbox problem, not a Kopia bug.&lt;/p&gt;
&lt;p&gt;Notes below assume KDE Plasma on Wayland (default Bazzite spin). The fix file itself is DE-agnostic; GNOME-specific deltas are called out where they matter.&lt;/p&gt;
&lt;h2 id=&quot;why-it-breaks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-it-breaks&quot;&gt;#&lt;/a&gt; Why it breaks&lt;/h2&gt;
&lt;p&gt;The toggle writes to &lt;code&gt;$XDG_CONFIG_HOME/autostart/kopia-ui.desktop&lt;/code&gt;. Under Flatpak, &lt;code&gt;$XDG_CONFIG_HOME&lt;/code&gt; is rewritten to the per-app sandbox directory (&lt;code&gt;~/.var/app/io.kopia.KopiaUI/config/&lt;/code&gt;), and the sandbox doesn&#39;t get &lt;code&gt;--filesystem=xdg-config/autostart:create&lt;/code&gt; by default. The file is &amp;quot;written&amp;quot; inside the sandbox where the session autostart loader never looks.&lt;/p&gt;
&lt;p&gt;The flatpak-exported launcher at &lt;code&gt;/var/lib/flatpak/exports/share/applications/io.kopia.KopiaUI.desktop&lt;/code&gt; is also a bad copy-paste source. Its &lt;code&gt;Exec=&lt;/code&gt; line is built for URL-handler launches:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Exec=/usr/bin/flatpak run --branch=stable --arch=x86_64 --command=kopia-ui --file-forwarding io.kopia.KopiaUI @@u %U @@
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--file-forwarding ... @@u %U @@&lt;/code&gt; block expects URL arguments. With no URLs at autostart, what actually happens depends on the session manager. Anything from a stray &amp;quot;Open File&amp;quot; prompt to silently failing to background.&lt;/p&gt;
&lt;p&gt;This is also why the click-path through Plasma&#39;s &lt;em&gt;System Settings → Autostart → Add Application&lt;/em&gt; doesn&#39;t save you: it copies the exported &lt;code&gt;.desktop&lt;/code&gt; into your autostart dir verbatim, broken &lt;code&gt;Exec&lt;/code&gt; and all. Same trap on GNOME&#39;s &lt;em&gt;Tweaks → Startup Applications&lt;/em&gt;. You need the hand-rolled file below.&lt;/p&gt;
&lt;h2 id=&quot;fix&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#fix&quot;&gt;#&lt;/a&gt; Fix&lt;/h2&gt;
&lt;p&gt;Drop a minimal autostart entry on the host:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# ~/.config/autostart/kopia-ui.desktop
[Desktop Entry]
Type=Application
Version=1.0
Name=KopiaUI
Comment=Fast and secure open source backup
Exec=flatpak run io.kopia.KopiaUI
StartupNotify=false
Terminal=false
X-GNOME-Autostart-enabled=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file is plain XDG autostart-spec, so Plasma&#39;s session manager picks it up directly from &lt;code&gt;~/.config/autostart/&lt;/code&gt; and runs it on login. The &lt;code&gt;X-GNOME-Autostart-enabled=true&lt;/code&gt; line is GNOME-specific extra metadata; Plasma ignores it, but leaving it in keeps the file portable if you also use a GNOME session somewhere.&lt;/p&gt;
&lt;p&gt;Verify after a reboot:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pgrep -af &#39;kopia-ui|io.kopia.KopiaUI&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see a &lt;code&gt;flatpak run&lt;/code&gt; wrapper and the actual &lt;code&gt;kopia-ui&lt;/code&gt; process under it.&lt;/p&gt;
&lt;h2 id=&quot;when-it-still-doesn-t-work&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#when-it-still-doesn-t-work&quot;&gt;#&lt;/a&gt; When it still doesn&#39;t work&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Plasma&#39;s &lt;em&gt;System Settings → Autostart&lt;/em&gt; lists it but it doesn&#39;t run.&lt;/strong&gt; Check &lt;code&gt;~/.config/autostart/kopia-ui.desktop&lt;/code&gt; perms. It must be readable by your user. &lt;code&gt;chmod 644&lt;/code&gt; it. Same applies to GNOME&#39;s &lt;em&gt;Tweaks → Startup Applications&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tray icon never appears on Plasma.&lt;/strong&gt; Plasma has native StatusNotifierItem support, so this should just work. If it doesn&#39;t, make sure the system tray widget is on your panel and configured to show all icons, not just an allow-list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tray icon never appears on GNOME.&lt;/strong&gt; GNOME has no native system tray. Install the &lt;strong&gt;AppIndicator and KStatusNotifierItem Support&lt;/strong&gt; extension. Without it, KopiaUI runs but minimises to nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kopia repo not unlocked at autostart.&lt;/strong&gt; By design. Kopia won&#39;t decrypt the repo without the password. Either configure the OS keyring integration in KopiaUI, or accept that you need to click through the unlock prompt once per session.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-not-the-in-app-toggle&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-not-the-in-app-toggle&quot;&gt;#&lt;/a&gt; Why not the in-app toggle&lt;/h2&gt;
&lt;p&gt;Even if a future Kopia release adds &lt;code&gt;--filesystem=xdg-config/autostart:create&lt;/code&gt; to its Flatpak manifest, the in-app toggle still produces the messy &lt;code&gt;Exec&lt;/code&gt; line from the exported launcher (it copies the system desktop entry verbatim). The Plasma and GNOME click-path autostart dialogs do the same thing. The hand-rolled file above is cleaner and survives Kopia upgrades because nothing on the package side touches &lt;code&gt;~/.config/autostart/&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;. Why Flatpak is the default install path on immutable Fedora.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/coolercontrol-autostart/&quot;&gt;Making CoolerControl&amp;apos;s GUI start on login&lt;/a&gt;. Same family of &amp;quot;Linux desktop autostart is annoying&amp;quot; problem, different root cause: rpm package ships no autostart entry at all, instead of one written into a sandboxed path.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/flatpak-1password-browser-bridge/&quot;&gt;Making 1Password browser extensions talk to the Flatpak desktop app&lt;/a&gt;. Same shape of problem: Flatpak sandbox isolation breaking an in-app feature that assumes host-level filesystem access.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/selinux-rsync-systemd-timer/&quot;&gt;Running rsync from a systemd timer on Bazzite &amp;lpar;SELinux rsync&amp;lowbar;t domain&amp;rpar;&lt;/a&gt;. If you&#39;re running Kopia alongside other backup automation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kopia.io/docs/installation/&quot;&gt;Kopia documentation — Linux installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html&quot;&gt;XDG Autostart specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.flatpak.org/en/latest/sandbox-permissions.html&quot;&gt;Flatpak sandbox permissions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="linux"/><category term="flatpak"/><category term="kopia"/><category term="backups"/><category term="bazzite"/><category term="kde"/><category term="plasma"/><category term="troubleshooting"/>
  </entry>
  
  <entry>
    <title>Running rsync from a systemd timer on Bazzite (SELinux rsync_t domain)</title>
    <link href="https://daal.cloud/blog/selinux-rsync-systemd-timer/"/>
    <id>https://daal.cloud/blog/selinux-rsync-systemd-timer/</id>
    <published>2026-04-28T00:00:00.000Z</published>
    <updated>2026-04-28T00:00:00.000Z</updated>
    <summary>You&#39;ll probably never need this exact setup. The reason it&#39;s written down is the SELinux mechanics underneath: domain transitions, audit2allow&#39;s blind spots, dontaudit, label inheritance, and how to actually figure out why SELinux is blocking you instead of just flipping a boolean.</summary>
    <content type="html">&lt;p&gt;Likely you will never do this. But the SELinux gotchas I hit along the way are worth writing down even if the original task isn&#39;t. So treat this less as a recipe and more as a tour of how SELinux actually behaves when something it didn&#39;t expect tries to read or write under &lt;code&gt;/home&lt;/code&gt;, and how to figure out &lt;em&gt;why&lt;/em&gt; it&#39;s blocking you instead of just flipping a boolean and moving on.&lt;/p&gt;
&lt;p&gt;For context: I don&#39;t normally run SELinux. On a desktop, why would you? ;-) The benefit-to-friction ratio just isn&#39;t there for me on a single-user box. But Bazzite ships it enforcing by default, and I&#39;d rather work with the policy than turn it off, so when this hit I leaned in.&lt;/p&gt;
&lt;p&gt;The setup that triggered it: two user accounts on the same Bazzite install, call them username1 and username2. Username2 already had a working backup. Username1 wanted their savegames included in it. The obvious move would be to set up backups on username1 too, but I already had this thing running on username2, so instead I wrote a systemd timer that runs rsync as root from username1&#39;s home into a folder under username2, and let username2&#39;s existing backup pick it up from there. I like a challenge but also don&#39;t want to reinvent the wheel. While writing this post I&#39;m already regretting it. But hey, here we are.&lt;/p&gt;
&lt;p&gt;And SELinux had thoughts.&lt;/p&gt;
&lt;p&gt;A systemd timer that runs &lt;code&gt;rsync&lt;/code&gt; as root does not behave like &lt;code&gt;rsync&lt;/code&gt; from a sudo shell, even though the UID is the same. Fedora&#39;s SELinux policy labels &lt;code&gt;/usr/bin/rsync&lt;/code&gt; as &lt;code&gt;rsync_exec_t&lt;/code&gt; and ships a &lt;code&gt;type_transition&lt;/code&gt; that moves the process into the confined &lt;code&gt;rsync_t&lt;/code&gt; domain whenever it&#39;s launched from a system context (&lt;code&gt;init_t&lt;/code&gt;, &lt;code&gt;initrc_t&lt;/code&gt;, etc). &lt;code&gt;rsync_t&lt;/code&gt; is intentionally narrow. It&#39;s the domain for the rsync server, not for ad-hoc file copies. Reading or writing anything under &lt;code&gt;/home&lt;/code&gt; or &lt;code&gt;/var/home&lt;/code&gt; is denied by default.&lt;/p&gt;
&lt;p&gt;A sudo shell stays in &lt;code&gt;unconfined_t&lt;/code&gt; and skips the transition. That&#39;s why &amp;quot;but it works when I run it by hand&amp;quot; is the usual entry point to this rabbit hole.&lt;/p&gt;
&lt;p&gt;Below: the Bazzite-specific traps I hit, plus how I cut a small local SELinux module for it instead of flipping the &lt;code&gt;rsync_full_access&lt;/code&gt; boolean.&lt;/p&gt;
&lt;h2 id=&quot;confirm-the-transition-is-happening&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#confirm-the-transition-is-happening&quot;&gt;#&lt;/a&gt; Confirm the transition is happening&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo ausearch -m AVC -ts recent | grep rsync_t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you see &lt;code&gt;scontext=system_u:system_r:rsync_t:s0&lt;/code&gt; paired with denied operations against &lt;code&gt;user_home_t&lt;/code&gt; or &lt;code&gt;data_home_t&lt;/code&gt;, that&#39;s the transition. Outside SELinux, a quick check is &lt;code&gt;cat /proc/&amp;lt;pid&amp;gt;/attr/current&lt;/code&gt; on the running rsync.&lt;/p&gt;
&lt;h2 id=&quot;two-ways-to-fix-it&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#two-ways-to-fix-it&quot;&gt;#&lt;/a&gt; Two ways to fix it&lt;/h2&gt;
&lt;h3 id=&quot;a-the-blunt-boolean&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#a-the-blunt-boolean&quot;&gt;#&lt;/a&gt; A. The blunt boolean&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo setsebool -P rsync_full_access on
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Persistent across reboots. Lets &lt;code&gt;rsync_t&lt;/code&gt; access most non-security files anywhere. Fine for a personal box, too wide for anything shared.&lt;/p&gt;
&lt;h3 id=&quot;b-a-scoped-local-policy-module&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#b-a-scoped-local-policy-module&quot;&gt;#&lt;/a&gt; B. A scoped local policy module&lt;/h3&gt;
&lt;p&gt;What you want when rsync only needs to touch a specific subtree.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Put just the &lt;code&gt;rsync_t&lt;/code&gt; domain into permissive so a full sync runs end-to-end and logs every denial it would have hit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo semanage permissive -a rsync_t
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Trigger the unit, ideally with a stale file in the destination so &lt;code&gt;--delete&lt;/code&gt; actually fires:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo touch /path/to/dest/STALE.tmp
sudo systemctl start your-rsync.service
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pull the AVCs and generate a module:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo ausearch -m AVC --start recent | audit2allow -M your_module
sudo semodule -i your_module.pp
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Drop permissive and verify under enforcing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo semanage permissive -d rsync_t
sudo systemctl start your-rsync.service
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For my use case (rsync mirror with &lt;code&gt;--delete&lt;/code&gt; and &lt;code&gt;--chown&lt;/code&gt;, source under one user&#39;s home, destination under another), the rules I needed were:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;allow rsync_t data_home_t:dir { getattr open read remove_name search setattr write add_name };
allow rsync_t data_home_t:file { getattr setattr unlink read open create write };
allow rsync_t gconf_home_t:dir search;
allow rsync_t user_home_dir_t:dir search;
allow rsync_t user_home_t:dir search;
allow rsync_t self:capability { dac_override chown fowner fsetid };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One thing that bit me: &lt;code&gt;data_home_t:file setattr&lt;/code&gt;. &lt;code&gt;audit2allow&lt;/code&gt; only suggests it once rsync has actually tried to update mtimes on an existing destination file, so the first permissive pass usually misses it and you find out the next time you run.&lt;/p&gt;
&lt;h2 id=&quot;debugging-traps-that-ate-hours&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#debugging-traps-that-ate-hours&quot;&gt;#&lt;/a&gt; Debugging traps that ate hours&lt;/h2&gt;
&lt;h3 id=&quot;audit2allow-only-sees-avcs-that-already-happened&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#audit2allow-only-sees-avcs-that-already-happened&quot;&gt;#&lt;/a&gt; audit2allow only sees AVCs that already happened&lt;/h3&gt;
&lt;p&gt;If rsync exits on the first denial, you only get that first denial in the audit log. Flip &lt;code&gt;rsync_t&lt;/code&gt; permissive, let the run finish end to end, then collect. Otherwise the module ships incomplete and every enforcing run after that hands you one more rule, one denial at a time.&lt;/p&gt;
&lt;h3 id=&quot;dontaudit-rules-silently-hide-denials&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#dontaudit-rules-silently-hide-denials&quot;&gt;#&lt;/a&gt; dontaudit rules silently hide denials&lt;/h3&gt;
&lt;p&gt;The default policy contains &lt;code&gt;dontaudit&lt;/code&gt; rules that suppress AVCs the policy author considered noise. When a denial is real but invisible:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo semodule -DB   # disable all dontaudit, rebuild policy
# ... reproduce, debug ...
sudo semodule -B    # restore
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;bazzite-s-default-audit-rule-suppresses-a-class-of-events&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#bazzite-s-default-audit-rule-suppresses-a-class-of-events&quot;&gt;#&lt;/a&gt; Bazzite&#39;s default audit rule suppresses a class of events&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;/etc/audit/rules.d/audit.rules&lt;/code&gt; ships with &lt;code&gt;-a never,task&lt;/code&gt;, which can hide events you&#39;d expect to see. If AVCs are still missing after disabling dontaudit:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo auditctl -D                  # clear runtime audit rules
# ... reproduce, ausearch, debug ...
sudo augenrules --load            # restore from rules.d
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;files-inherit-the-label-of-whichever-process-created-them&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#files-inherit-the-label-of-whichever-process-created-them&quot;&gt;#&lt;/a&gt; Files inherit the label of whichever process created them&lt;/h3&gt;
&lt;p&gt;If your initial seed ran from a sudo shell (&lt;code&gt;unconfined_t&lt;/code&gt;) and the later periodic runs come from the timer (&lt;code&gt;rsync_t&lt;/code&gt;), the destination directory and the files inside can end up with different SELinux types. The dir-class denials in &lt;code&gt;audit2allow&lt;/code&gt; won&#39;t match the file-class denials, which is confusing for about an hour. Fix with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo chcon -R -t data_home_t /path/to/dest
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;authoring-a-te-file-by-hand&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#authoring-a-te-file-by-hand&quot;&gt;#&lt;/a&gt; Authoring a &lt;code&gt;.te&lt;/code&gt; file by hand&lt;/h3&gt;
&lt;p&gt;When you want to skip &lt;code&gt;audit2allow&lt;/code&gt; and write the rules yourself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo checkmodule -M -m -o your_module.mod your_module.te
sudo semodule_package -o your_module.pp -m your_module.mod
sudo semodule -i your_module.pp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sudo semodule -l | grep your_module&lt;/code&gt; confirms install. &lt;code&gt;sudo semodule -r your_module&lt;/code&gt; removes it.&lt;/p&gt;
&lt;h2 id=&quot;bazzite-specifics-worth-flagging&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#bazzite-specifics-worth-flagging&quot;&gt;#&lt;/a&gt; Bazzite specifics worth flagging&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;rsync_t&lt;/code&gt; transition is Fedora policy, nothing Bazzite-specific. Same workflow applies on Silverblue, Kinoite, Bluefin, plain Fedora, and RHEL.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/var/lib/selinux/targeted/active/modules&lt;/code&gt; isn&#39;t where modules live on rpm-ostree systems; storage sits elsewhere under the ostree deployment. Use &lt;code&gt;semodule -l&lt;/code&gt; and &lt;code&gt;semodule -E &amp;lt;name&amp;gt;&lt;/code&gt; to inspect rather than &lt;code&gt;find&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Audit rules persist across &lt;code&gt;rpm-ostree&lt;/code&gt; upgrades because &lt;code&gt;/etc&lt;/code&gt; is a real directory, not part of the immutable image.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-bother-with-the-module-instead-of-the-boolean&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-bother-with-the-module-instead-of-the-boolean&quot;&gt;#&lt;/a&gt; Why bother with the module instead of the boolean&lt;/h2&gt;
&lt;p&gt;The boolean would have saved me hours. The module is safer: it lists exactly which types &lt;code&gt;rsync_t&lt;/code&gt; is allowed to touch and nothing else. On a personal machine the boolean is probably fine. If you hit this on a server, now you know how to fix it. Although... if you really want to copy things between Username1 and Username2 on a server, I have other questions for you ;-)&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/ssm-shell-apparmor-systemd-run/&quot;&gt;Escaping the AWS SSM AppArmor profile with systemd-run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/rpm-ostree-rebase-cheatsheet/&quot;&gt;rpm-ostree&amp;colon; rebase&amp;comma; pin&amp;comma; rollback&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.fedoraproject.org/en-US/quick-docs/selinux/&quot;&gt;SELinux User&#39;s and Administrator&#39;s Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.die.net/man/1/audit2allow&quot;&gt;&lt;code&gt;audit2allow(1)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://linux.die.net/man/8/semanage&quot;&gt;&lt;code&gt;semanage(8)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/8/html/using_selinux/writing-a-custom-selinux-policy_using-selinux&quot;&gt;Writing a custom SELinux policy (Red Hat docs)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="linux"/><category term="bazzite"/><category term="selinux"/><category term="systemd"/><category term="rsync"/><category term="troubleshooting"/>
  </entry>
  
  <entry>
    <title>Escaping the AWS SSM AppArmor profile with systemd-run</title>
    <link href="https://daal.cloud/blog/ssm-shell-apparmor-systemd-run/"/>
    <id>https://daal.cloud/blog/ssm-shell-apparmor-systemd-run/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>On Ubuntu, SSM Session Manager shells inherit the snap-amazon-ssm-agent AppArmor profile. Even root hits silent EACCES on writes. Escape via systemd-run.</summary>
    <content type="html">&lt;p&gt;On Ubuntu via &lt;a href=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&quot;&gt;SSM Session Manager&lt;/a&gt;, your shell (and every child it spawns) runs under the &lt;code&gt;snap.amazon-ssm-agent.amazon-ssm-agent&lt;/code&gt; AppArmor profile. uid 0 with &lt;code&gt;CAP_DAC_OVERRIDE&lt;/code&gt; doesn&#39;t get out of it. Writes to paths like &lt;code&gt;/var/log/postgresql/*&lt;/code&gt; return &lt;code&gt;EACCES&lt;/code&gt; even with a clean &lt;code&gt;lsattr&lt;/code&gt;, a writable fs, and the right caps.&lt;/p&gt;
&lt;h2 id=&quot;confirm-it-s-apparmor&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#confirm-it-s-apparmor&quot;&gt;#&lt;/a&gt; Confirm it&#39;s AppArmor&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;$ cat /proc/self/attr/current
snap.amazon-ssm-agent.amazon-ssm-agent (enforce)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dmesg&lt;/code&gt; will probably stay quiet. Snap profiles use &lt;code&gt;audit deny&lt;/code&gt; for &amp;quot;expected&amp;quot; paths, which suppresses the kernel audit line. When entries do show up they can lag a couple of minutes.&lt;/p&gt;
&lt;h2 id=&quot;escape-via-systemd-run&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#escape-via-systemd-run&quot;&gt;#&lt;/a&gt; Escape via systemd-run&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt; runs as PID 1, outside any AppArmor profile. Anything it starts is unconfined. &lt;code&gt;systemd-run --pipe --wait&lt;/code&gt; delegates a command to PID 1 and pipes the result back:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo systemd-run --pipe --wait &amp;lt;cmd&amp;gt; [args...]
sudo systemd-run --pipe --wait --uid=postgres bash -c &#39;truncate -s 0 /var/log/postgresql/postgresql-16-main.log&#39;
sudo systemd-run --pipe --wait tee /etc/foo/bar.conf &amp;lt;&amp;lt;&#39;EOF&#39;
some content
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--pipe&lt;/code&gt; forwards stdin/stdout/stderr.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--wait&lt;/code&gt; is synchronous and propagates the exit code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--uid=&amp;lt;user&amp;gt;&lt;/code&gt; drops privilege. PID 1 can become any uid.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;better-alternatives&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#better-alternatives&quot;&gt;#&lt;/a&gt; Better alternatives&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SSH with a real keypair.&lt;/strong&gt; SSH sessions don&#39;t inherit the snap profile. Worth the setup if you touch the box more than occasionally.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-bother&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-bother&quot;&gt;#&lt;/a&gt; Why bother&lt;/h2&gt;
&lt;p&gt;SSM is the right tool for ad-hoc ops without exposing SSH, or when you&#39;ve lost the original key. It&#39;s also audited, which is nice. Knowing about the AppArmor inheritance saves you the hour of wondering why root can&#39;t write to a directory it owns (like I did).&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/podman-vs-docker/&quot;&gt;Living without docker&amp;colon; podman as a daily driver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/mcp-env-vars-not-from-bashrc/&quot;&gt;Why your MCP server cannot see env vars from &amp;period;bashrc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ubuntu.com/server/docs/security-apparmor&quot;&gt;AppArmor in Ubuntu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/systemd-run.html&quot;&gt;&lt;code&gt;systemd-run(1)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html&quot;&gt;AWS SSM Session Manager&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="aws"/><category term="ssm"/><category term="apparmor"/><category term="linux"/><category term="ubuntu"/><category term="troubleshooting"/>
  </entry>
  
  <entry>
    <title>Why your MCP server cannot see env vars from .bashrc</title>
    <link href="https://daal.cloud/blog/mcp-env-vars-not-from-bashrc/"/>
    <id>https://daal.cloud/blog/mcp-env-vars-not-from-bashrc/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>Claude Code spawns MCP servers as plain child processes. They never source .bashrc. Set MCP env vars in environment.d or the MCP env config, not your shell rc files.</summary>
    <content type="html">&lt;p&gt;You add an &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;MCP&lt;/a&gt; server to &lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/overview&quot;&gt;Claude Code&lt;/a&gt; and it requires an environment variable. But the server doesn&#39;t show up when you run &lt;code&gt;/mcp&lt;/code&gt;, no error surfaces, nothing useful in the logs. Nine times out of ten: an env var it needs is set in &lt;code&gt;~/.bashrc&lt;/code&gt;, and nothing Claude Code launches will ever see it.&lt;/p&gt;
&lt;h2 id=&quot;the-mechanic&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-mechanic&quot;&gt;#&lt;/a&gt; The mechanic&lt;/h2&gt;
&lt;p&gt;Claude Code spawns each MCP server as a plain child process. No interactive shell, no &lt;code&gt;.bashrc&lt;/code&gt; sourcing. &lt;code&gt;.bashrc&lt;/code&gt; is for interactive shells only. Non-interactive children inherit the parent&#39;s already-resolved environment, not the file.&lt;/p&gt;
&lt;p&gt;So this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# ~/.bashrc
export MY_API_KEY=sk-xxxxxxxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is invisible to MCP servers, hooks, and skill scripts. The server starts, can&#39;t read the var, exits silently. Claude Code only sees &amp;quot;didn&#39;t initialise.&amp;quot;&lt;/p&gt;
&lt;h2 id=&quot;where-to-put-env-vars-instead&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#where-to-put-env-vars-instead&quot;&gt;#&lt;/a&gt; Where to put env vars instead&lt;/h2&gt;
&lt;p&gt;In rough order of preference:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;systemd &lt;code&gt;environment.d&lt;/code&gt;&lt;/strong&gt; (Linux, systemd-based distros). Drop &lt;code&gt;~/.config/environment.d/mcp.conf&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MY_API_KEY=sk-xxxxxxxx
ANTHROPIC_LOG=debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are part of the user session, loaded by &lt;code&gt;systemd --user&lt;/code&gt;, inherited by every desktop, IDE, terminal, and Claude Code process spawned from that session. Log out / log in to apply, or &lt;code&gt;systemctl --user daemon-reload&lt;/code&gt; and relog.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The MCP server&#39;s own &lt;code&gt;env&lt;/code&gt; config.&lt;/strong&gt; In &lt;code&gt;~/.claude/settings.json&lt;/code&gt; or project &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;mcpServers&amp;quot;: {
    &amp;quot;myserver&amp;quot;: {
      &amp;quot;command&amp;quot;: &amp;quot;/path/to/server&amp;quot;,
      &amp;quot;env&amp;quot;: { &amp;quot;MY_API_KEY&amp;quot;: &amp;quot;sk-xxxxxxxx&amp;quot; }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Explicit and per-server. Downside: secrets end up in a config file. Only safe if it isn&#39;t synced or committed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;~/.profile&lt;/code&gt; or &lt;code&gt;~/.zprofile&lt;/code&gt;&lt;/strong&gt;. Login shells source these, and most display managers do start the user session via a login shell. Cleaner than &lt;code&gt;.bashrc&lt;/code&gt; but more fragile across distros.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;launchctl setenv&lt;/code&gt;&lt;/strong&gt; on macOS. Persists only inside the current &lt;code&gt;launchd&lt;/code&gt; session unless you wire a &lt;code&gt;LaunchAgent&lt;/code&gt; plist.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;confirming-the-failure-mode&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#confirming-the-failure-mode&quot;&gt;#&lt;/a&gt; Confirming the failure mode&lt;/h2&gt;
&lt;p&gt;When an MCP server is missing from &lt;code&gt;/mcp&lt;/code&gt; and you suspect env vars are the cause:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Run the binary from a non-interactive shell to reproduce:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;bash -c &#39;/path/to/mcp-server&#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An interactive shell sources &lt;code&gt;.bashrc&lt;/code&gt;. &lt;code&gt;bash -c&lt;/code&gt; doesn&#39;t. If it fails here, it&#39;ll fail when Claude Code spawns it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add a startup log line that prints &lt;code&gt;os.environ.get(&amp;quot;MY_API_KEY&amp;quot;, &amp;quot;(missing)&amp;quot;)&lt;/code&gt;. Fastest way to confirm the var isn&#39;t reaching the child.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;child-processes-not-seeing-what-you-d-expect-is-a-recurring-failure-mode&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#child-processes-not-seeing-what-you-d-expect-is-a-recurring-failure-mode&quot;&gt;#&lt;/a&gt; Child processes not seeing what you&#39;d expect is a recurring failure mode&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/ssm-shell-apparmor-systemd-run/&quot;&gt;Escaping the AWS SSM AppArmor profile with systemd-run&lt;/a&gt;. SSM child shells inherit an AppArmor profile silently.&lt;/li&gt;
&lt;li&gt;Cron with no &lt;code&gt;$PATH&lt;/code&gt; because cron&#39;s environment is a stripped-down version of root&#39;s login env.&lt;/li&gt;
&lt;li&gt;Docker build steps not seeing &lt;code&gt;$DOCKER_BUILDKIT&lt;/code&gt; because the daemon, not your shell, holds it. (See &lt;a href=&quot;/blog/podman-vs-docker/&quot;&gt;Living without docker&amp;colon; podman as a daily driver&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In every case, don&#39;t assume your shell&#39;s environment is what the child process actually sees. Set the var where the child picks it up.&lt;/p&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://modelcontextprotocol.io/docs/specification/server&quot;&gt;Model Context Protocol — server configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/environment.d.html&quot;&gt;&lt;code&gt;environment.d(5)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/mcp&quot;&gt;Claude Code MCP docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="claude"/><category term="mcp"/><category term="claude-code"/><category term="linux"/><category term="systemd"/><category term="troubleshooting"/>
  </entry>
  
  <entry>
    <title>Pagefind: full-text search for static sites, no backend</title>
    <link href="https://daal.cloud/blog/pagefind-client-side-search/"/>
    <id>https://daal.cloud/blog/pagefind-client-side-search/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>Pagefind builds a chunked search index at build time and queries it from the browser via WebAssembly. Zero servers, zero queries leaving the page, and it scales to thousands of pages without shipping the whole index up front.</summary>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://pagefind.app/&quot;&gt;Pagefind&lt;/a&gt; is a search library for static sites. It indexes the built HTML once at build time and serves the index as static files. Queries run in the visitor&#39;s browser against a small WASM binary - no search backend, no API key, no third-party JS.&lt;/p&gt;
&lt;p&gt;This site uses it for the &lt;code&gt;/blog/&lt;/code&gt; index.&lt;/p&gt;
&lt;h2 id=&quot;how-it-works&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#how-it-works&quot;&gt;#&lt;/a&gt; How it works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Build step.&lt;/strong&gt; After your site generator writes HTML, you run &lt;code&gt;pagefind --site _site&lt;/code&gt;. Pagefind crawls the output, looks for &lt;code&gt;data-pagefind-body&lt;/code&gt; (or just falls back to &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt;), tokenises the text, and writes:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pagefind/pagefind.js&lt;/code&gt; - the loader.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pagefind/pagefind-ui.js&lt;/code&gt; + &lt;code&gt;pagefind-ui.css&lt;/code&gt; - the optional default UI (a search input + result list).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pagefind/wasm.*.pagefind&lt;/code&gt; - the &lt;a href=&quot;https://webassembly.org/&quot;&gt;WASM binary&lt;/a&gt; that does the actual querying.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pagefind/index/*.pagefind&lt;/code&gt; - the index, &lt;a href=&quot;https://pagefind.app/docs/architecture/&quot;&gt;chunked by term prefix&lt;/a&gt; (typically a few hundred bytes to a few KB per chunk).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pagefind/fragment/*.pagefind&lt;/code&gt; - per-page metadata (title, URL, excerpt source).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Page load.&lt;/strong&gt; The browser loads &lt;code&gt;pagefind-ui.js&lt;/code&gt; lazily. Nothing else fetches yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;First keystroke.&lt;/strong&gt; Pagefind loads the WASM (~70KB gzip), then fetches only the index chunks matching the prefix the user typed. For a query like &lt;code&gt;podm&lt;/code&gt;, it pulls the &lt;code&gt;pod&lt;/code&gt; chunk, not the whole index.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Result render.&lt;/strong&gt; Matched fragment files are fetched to build excerpts. Each result is one or two extra ~500-byte requests.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The total network cost for a typical search is well under 200KB even on a multi-thousand-page site, because nothing is loaded until the user actually searches and only the relevant chunks come down.&lt;/p&gt;
&lt;h2 id=&quot;why-it-beats-a-backend-search-service&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-it-beats-a-backend-search-service&quot;&gt;#&lt;/a&gt; Why it beats a backend search service&lt;/h2&gt;
&lt;p&gt;For a static site or a personal KB, Pagefind cuts out a bunch of operational headaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No server.&lt;/strong&gt; Nothing to provision, monitor, rate-limit, patch, or pay for. It&#39;s bytes on a CDN. CF Pages / GitHub Pages / S3 / Netlify all serve it the same way they serve the rest of your site.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No third-party.&lt;/strong&gt; Algolia/Typesense/Elastic-as-a-service all add a domain to your CSP, a tracking surface, an SLA you depend on, and a key that can leak. Pagefind queries never leave the page - relevant if you write about anything you&#39;d rather not hand to a vendor&#39;s logs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private by design.&lt;/strong&gt; No analytics. No &amp;quot;what did this user search for&amp;quot; telemetry to leak. There&#39;s no place for it to go, since the &lt;a href=&quot;https://pagefind.app/docs/security/&quot;&gt;search runs entirely in the page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Builds in the same pipeline.&lt;/strong&gt; It&#39;s one extra command after your site generator. No separate index-rebuild job, no webhook, no eventual-consistency window between &amp;quot;I published a page&amp;quot; and &amp;quot;it&#39;s searchable.&amp;quot; When the site deploys, the index deploys.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trivial to roll back.&lt;/strong&gt; A bad index ships with a bad deploy and reverts with a &lt;code&gt;git revert&lt;/code&gt; + redeploy. With a hosted index, rollback means re-indexing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cheap.&lt;/strong&gt; &lt;a href=&quot;https://github.com/CloudCannon/pagefind&quot;&gt;Free.&lt;/a&gt; MIT-licensed. The hosting cost is whatever your static host charges (often $0).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-a-backend-still-wins&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#where-a-backend-still-wins&quot;&gt;#&lt;/a&gt; Where a backend still wins&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tens of thousands of pages.&lt;/strong&gt; Pagefind chunks the index but the &lt;a href=&quot;https://pagefind.app/docs/indexing/&quot;&gt;fragment count grows linearly&lt;/a&gt;. Past ~10k pages, the build step gets slow and the cold-start WASM + chunk fetches start to feel sluggish on mobile data. Backend search keeps a constant client cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-user authorisation.&lt;/strong&gt; If results depend on who&#39;s logged in (private docs, multi-tenant SaaS), the index can&#39;t be public, and shipping a per-user index isn&#39;t realistic. You need a server enforcing access at query time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Frequent index updates without a deploy.&lt;/strong&gt; If content changes outside the build (user-generated, CMS-driven, real-time), you need an index that updates independently. Pagefind only updates when you rebuild.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fancy features.&lt;/strong&gt; Synonyms, typo-tolerance with a learned language model, query analytics, A/B-tested ranking, federated search across heterogeneous sources - all things &lt;a href=&quot;https://www.algolia.com/&quot;&gt;Algolia&lt;/a&gt; / &lt;a href=&quot;https://typesense.org/&quot;&gt;Typesense&lt;/a&gt; / &lt;a href=&quot;https://www.meilisearch.com/&quot;&gt;Meilisearch&lt;/a&gt; do well and Pagefind doesn&#39;t try to.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;integration-notes-eleventy-specifically&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#integration-notes-eleventy-specifically&quot;&gt;#&lt;/a&gt; Integration notes (Eleventy specifically)&lt;/h2&gt;
&lt;p&gt;Three things to remember:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pagefind has no watch mode.&lt;/strong&gt; &lt;code&gt;eleventy --serve&lt;/code&gt; keeps the build in memory and never invokes Pagefind. For dev, &lt;code&gt;npm run dev&lt;/code&gt; shows you the empty &lt;code&gt;#search&lt;/code&gt; div with no UI; it&#39;s not broken. To actually exercise search, build to disk and serve the directory: &lt;code&gt;npm run build &amp;amp;&amp;amp; npm run serve&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Indexing scope.&lt;/strong&gt; By default Pagefind indexes everything inside &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. Wrap your real content in &lt;code&gt;&amp;lt;main data-pagefind-body&amp;gt;&lt;/code&gt; (or &lt;code&gt;&amp;lt;article data-pagefind-body&amp;gt;&lt;/code&gt;) to exclude nav/footer chrome from search results. Worth doing - otherwise every result excerpt starts with &amp;quot;home blog Dark mode ☾&amp;quot;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI customisation through CSS variables.&lt;/strong&gt; The default &lt;code&gt;PagefindUI&lt;/code&gt; widget exposes &lt;code&gt;--pagefind-ui-primary&lt;/code&gt;, &lt;code&gt;--pagefind-ui-background&lt;/code&gt;, &lt;code&gt;--pagefind-ui-border&lt;/code&gt;, etc. You set them on &lt;code&gt;:root&lt;/code&gt; and the widget picks them up. The override block in &lt;code&gt;css/style.css:326&lt;/code&gt; is all this site does to theme it.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/mcp-env-vars-not-from-bashrc/&quot;&gt;Why your MCP server cannot see env vars from &amp;period;bashrc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://pagefind.app/&quot;&gt;Pagefind documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pagefind.app/docs/architecture/&quot;&gt;Pagefind architecture overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/CloudCannon/pagefind&quot;&gt;Pagefind on GitHub (CloudCannon)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="pagefind"/><category term="search"/><category term="static-sites"/><category term="eleventy"/><category term="devtools"/>
  </entry>
  
  <entry>
    <title>Living without docker: podman as a daily driver</title>
    <link href="https://daal.cloud/blog/podman-vs-docker/"/>
    <id>https://daal.cloud/blog/podman-vs-docker/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>Podman is a near drop-in replacement for the Docker CLI. Daemonless, rootless by default, ships in immutable distros. Muscle-memory translation and the few places the abstraction leaks.</summary>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://podman.io/&quot;&gt;Podman&lt;/a&gt; ships on most modern Linux desktops, &lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&lt;/a&gt; included. The CLI is intentionally &lt;code&gt;docker&lt;/code&gt;-compatible (most subcommands and flags work identically), but two things differ under the hood:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No daemon.&lt;/strong&gt; Podman invokes containers directly via &lt;code&gt;runc&lt;/code&gt;/&lt;code&gt;crun&lt;/code&gt;. There&#39;s no long-running root service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rootless by default.&lt;/strong&gt; Each user has their own container store under &lt;code&gt;~/.local/share/containers/&lt;/code&gt;. Containers run mapped to your UID via &lt;a href=&quot;https://docs.kernel.org/admin-guide/namespaces/index.html&quot;&gt;user namespaces&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both are a security win. They also remove the &amp;quot;is the daemon running?&amp;quot; and &amp;quot;is my user in the &lt;code&gt;docker&lt;/code&gt; group?&amp;quot; failure modes.&lt;/p&gt;
&lt;h2 id=&quot;common-docker-commands-as-podman-commands&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#common-docker-commands-as-podman-commands&quot;&gt;#&lt;/a&gt; Common docker commands as podman commands&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker pull alpine          →   podman pull alpine
docker run -it alpine sh    →   podman run -it alpine sh
docker build -t foo .       →   podman build -t foo .
docker compose up           →   podman compose up        # via podman-compose
docker ps                   →   podman ps
docker exec -it &amp;lt;id&amp;gt; sh     →   podman exec -it &amp;lt;id&amp;gt; sh
docker logs -f &amp;lt;id&amp;gt;         →   podman logs -f &amp;lt;id&amp;gt;
docker network ls           →   podman network ls
docker volume create x      →   podman volume create x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your fingers keep typing &lt;code&gt;docker&lt;/code&gt;, install &lt;a href=&quot;https://packages.fedoraproject.org/pkgs/podman/podman-docker/&quot;&gt;&lt;code&gt;podman-docker&lt;/code&gt;&lt;/a&gt;. It symlinks &lt;code&gt;/usr/bin/docker&lt;/code&gt; to &lt;code&gt;/usr/bin/podman&lt;/code&gt; so old scripts keep working. Bazzite ships it by default.&lt;/p&gt;
&lt;h2 id=&quot;where-the-abstraction-leaks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#where-the-abstraction-leaks&quot;&gt;#&lt;/a&gt; Where the abstraction leaks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;docker compose&lt;/code&gt; v2 vs &lt;code&gt;podman-compose&lt;/code&gt;.&lt;/strong&gt; Parity is close but not perfect. The &lt;a href=&quot;https://github.com/containers/podman-compose&quot;&gt;compose plugin shipped with Podman 5.x&lt;/a&gt; handles most multi-container apps. If you hit a wall, &lt;a href=&quot;https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html&quot;&gt;Quadlet&lt;/a&gt; units are closer to how production actually runs anyway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker socket.&lt;/strong&gt; Tools that hardcode &lt;code&gt;/var/run/docker.sock&lt;/code&gt; need a podman socket. Run &lt;code&gt;systemctl --user enable --now podman.socket&lt;/code&gt;, then point them at &lt;code&gt;DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NVIDIA passthrough.&lt;/strong&gt; &lt;a href=&quot;https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html&quot;&gt;The container toolkit&lt;/a&gt; supports both runtimes. The podman path needs &lt;code&gt;--security-opt=label=disable&lt;/code&gt; on SELinux distros.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buildkit features.&lt;/strong&gt; &lt;code&gt;docker build --secret&lt;/code&gt;, &lt;code&gt;--ssh&lt;/code&gt;, &lt;code&gt;--mount=type=cache&lt;/code&gt; all have podman equivalents. Flag names differ slightly. Check &lt;code&gt;podman build --help&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;macOS / Windows.&lt;/strong&gt; Podman runs a Linux VM under the hood (&lt;code&gt;podman machine&lt;/code&gt;). Resource limits and bind mounts work, but &lt;code&gt;host.docker.internal&lt;/code&gt; becomes &lt;code&gt;host.containers.internal&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;why-it-matters-on-immutable-distros&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-it-matters-on-immutable-distros&quot;&gt;#&lt;/a&gt; Why it matters on immutable distros&lt;/h2&gt;
&lt;p&gt;On &lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&lt;/a&gt; and other rpm-ostree systems, you avoid layering RPMs into the base image (see &lt;a href=&quot;/blog/rpm-ostree-rebase-cheatsheet/&quot;&gt;rpm-ostree&amp;colon; rebase&amp;comma; pin&amp;comma; rollback&lt;/a&gt;). Containers are how you install traditional CLI tooling. &lt;code&gt;distrobox&lt;/code&gt; and &lt;code&gt;toolbx&lt;/code&gt; both wrap Podman to give you a transparent shell into a container with your home directory mounted in. That&#39;s where &lt;code&gt;gcc&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;terraform&lt;/code&gt;, etc. should live.&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/rpm-ostree-rebase-cheatsheet/&quot;&gt;rpm-ostree&amp;colon; rebase&amp;comma; pin&amp;comma; rollback&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.podman.io/&quot;&gt;Podman documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="containers"/><category term="podman"/><category term="docker"/><category term="linux"/><category term="devtools"/><category term="bazzite"/>
  </entry>
  
  <entry>
    <title>Bazzite: an immutable gaming-first Fedora variant</title>
    <link href="https://daal.cloud/blog/bazzite-overview/"/>
    <id>https://daal.cloud/blog/bazzite-overview/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>Short orientation to Bazzite. What it is, why the immutable layout matters, how it changes day-to-day system management.</summary>
    <content type="html">&lt;p&gt;&lt;a href=&quot;https://bazzite.gg/&quot;&gt;Bazzite&lt;/a&gt; is a community-built Linux distribution derived from Fedora&#39;s atomic desktop family (&lt;a href=&quot;https://fedoraproject.org/atomic-desktops/silverblue/&quot;&gt;Silverblue&lt;/a&gt;, &lt;a href=&quot;https://fedoraproject.org/atomic-desktops/kinoite/&quot;&gt;Kinoite&lt;/a&gt;) with a heavy focus on gaming, handhelds (Steam Deck, ROG Ally), and home-theatre boxes. Ships with the Steam stack, &lt;a href=&quot;https://www.mesa3d.org/&quot;&gt;Mesa&lt;/a&gt; drivers, Proton-tuned kernels, and &lt;code&gt;gamescope&lt;/code&gt; preconfigured.&lt;/p&gt;
&lt;h2 id=&quot;mental-model&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#mental-model&quot;&gt;#&lt;/a&gt; Mental model&lt;/h2&gt;
&lt;p&gt;Two things change relative to a traditional Fedora install:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The base system is image-based and read-only.&lt;/strong&gt; Updates are atomic. You boot into a new commit or roll back to the previous one. There&#39;s no &lt;code&gt;dnf install&lt;/code&gt; of arbitrary RPMs into the running system. The OS image is built upstream and you rebase between images.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User software lives in containers and Flatpaks.&lt;/strong&gt; &lt;a href=&quot;https://flatpak.org/&quot;&gt;Flatpak&lt;/a&gt; for graphical apps, &lt;a href=&quot;https://distrobox.it/&quot;&gt;&lt;code&gt;distrobox&lt;/code&gt;&lt;/a&gt; and &lt;a href=&quot;https://containertoolbx.org/&quot;&gt;&lt;code&gt;toolbx&lt;/code&gt;&lt;/a&gt; for traditional CLI tooling, &lt;a href=&quot;https://brew.sh/&quot;&gt;Homebrew&lt;/a&gt; for one-shot CLI packages.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That split is why Bazzite is hard to brick (you can break the user side without touching boot).&lt;/p&gt;
&lt;h2 id=&quot;day-to-day&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#day-to-day&quot;&gt;#&lt;/a&gt; Day-to-day&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Update.&lt;/strong&gt; &lt;code&gt;rpm-ostree upgrade&lt;/code&gt;, or &lt;code&gt;ujust update&lt;/code&gt; on Bazzite (which wraps the same machinery and also refreshes Flatpaks). Reboot to apply. See &lt;a href=&quot;/blog/rpm-ostree-rebase-cheatsheet/&quot;&gt;rpm-ostree&amp;colon; rebase&amp;comma; pin&amp;comma; rollback&lt;/a&gt; for rebase, pin, and rollback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install a GUI app.&lt;/strong&gt; Prefer Flatpak: &lt;code&gt;flatpak install flathub &amp;lt;app&amp;gt;&lt;/code&gt;. Desktop integration is fine for almost everything. Notable exception is browser → 1Password. See &lt;a href=&quot;/blog/flatpak-1password-browser-bridge/&quot;&gt;Making 1Password browser extensions talk to the Flatpak desktop app&lt;/a&gt; for some notes on that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install a CLI tool.&lt;/strong&gt; Spin up a &lt;code&gt;toolbox&lt;/code&gt; or &lt;code&gt;distrobox&lt;/code&gt; container running Fedora or Arch, install the tool inside, export the binary. Containers are throwaway. Destroy and recreate when you want a clean state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container runtime.&lt;/strong&gt; Bazzite ships &lt;a href=&quot;https://podman.io/&quot;&gt;Podman&lt;/a&gt;, not Docker. See also &lt;a href=&quot;/blog/podman-vs-docker/&quot;&gt;Living without docker&amp;colon; podman as a daily driver&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;when-troubleshooting&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#when-troubleshooting&quot;&gt;#&lt;/a&gt; When troubleshooting&lt;/h2&gt;
&lt;p&gt;&amp;quot;Where does this thing live?&amp;quot; is harder on Bazzite than on stock Fedora. A given binary is in one of four places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Image layer.&lt;/strong&gt; Read-only at &lt;code&gt;/usr&lt;/code&gt;. Changes only via &lt;code&gt;rpm-ostree&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layered RPM.&lt;/strong&gt; Added via &lt;code&gt;rpm-ostree install&lt;/code&gt;. Listed by &lt;code&gt;rpm-ostree status -v&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flatpak.&lt;/strong&gt; Sandboxed under &lt;code&gt;/var/lib/flatpak&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Container.&lt;/strong&gt; Inside &lt;code&gt;distrobox&lt;/code&gt; / &lt;code&gt;toolbox&lt;/code&gt;. Invisible to the host package manager.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Knowing which layer a misbehaving binary lives in tells you which tool fixes it. A 1Password browser plugin failing to talk to the desktop app is almost always a Flatpak permission / host-bridge problem, not a system one.&lt;/p&gt;
&lt;h2 id=&quot;privacy-footnote&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#privacy-footnote&quot;&gt;#&lt;/a&gt; Privacy footnote&lt;/h2&gt;
&lt;p&gt;Everything in this note is public Bazzite behaviour, nothing specific to my install. For the authoritative reference, &lt;code&gt;man rpm-ostree&lt;/code&gt; and the &lt;a href=&quot;https://docs.bazzite.gg/&quot;&gt;Bazzite docs&lt;/a&gt;.&lt;/p&gt;
</content>
    <category term="linux"/><category term="bazzite"/><category term="immutable-os"/><category term="gaming"/><category term="fedora"/>
  </entry>
  
  <entry>
    <title>Making 1Password browser extensions talk to the Flatpak desktop app</title>
    <link href="https://daal.cloud/blog/flatpak-1password-browser-bridge/"/>
    <id>https://daal.cloud/blog/flatpak-1password-browser-bridge/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>Browser extension can&#39;t see a Flatpak 1Password desktop app via native messaging. Sandbox isolation is the cause. Fix is the explicit cross-sandbox bridge 1Password ships.</summary>
    <content type="html">&lt;p&gt;If you install the &lt;a href=&quot;https://1password.com/downloads/linux/&quot;&gt;1Password desktop app&lt;/a&gt; as a Flatpak (the recommended path on &lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&lt;/a&gt; and other immutable distros), and your browser is also a Flatpak, &amp;quot;Connect to 1Password&amp;quot; hangs. The unlock screen sits forever, or the extension claims the desktop app isn&#39;t running even though it clearly is.&lt;/p&gt;
&lt;p&gt;Sandboxing problem, not a 1Password bug.&lt;/p&gt;
&lt;h2 id=&quot;why-it-breaks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-it-breaks&quot;&gt;#&lt;/a&gt; Why it breaks&lt;/h2&gt;
&lt;p&gt;The desktop app and the extension talk via &lt;a href=&quot;https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging&quot;&gt;native messaging&lt;/a&gt;. The browser launches a small helper binary (&lt;code&gt;com.1password.1password.json&lt;/code&gt; on Linux) that opens a Unix socket inside the desktop app&#39;s runtime directory. Normal install: both sides share &lt;code&gt;/run/user/&amp;lt;uid&amp;gt;/&lt;/code&gt;, they find each other.&lt;/p&gt;
&lt;p&gt;Under Flatpak, each app runs in its own sandbox. The browser sandbox can&#39;t see 1Password&#39;s &lt;code&gt;/run/user/&amp;lt;uid&amp;gt;/&lt;/code&gt; by default. Even if it could, the &lt;code&gt;NativeMessagingHosts&lt;/code&gt; manifest path the browser scans (&lt;code&gt;~/.config/&amp;lt;browser&amp;gt;/NativeMessagingHosts/&lt;/code&gt;) is rewritten by the sandbox to a Flatpak-private location.&lt;/p&gt;
&lt;p&gt;Net result: browser never sees the manifest, never spawns the helper, never finds the socket.&lt;/p&gt;
&lt;h2 id=&quot;fix&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#fix&quot;&gt;#&lt;/a&gt; Fix&lt;/h2&gt;
&lt;p&gt;1Password ships a cross-sandbox bridge. Set it up once:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Make the manifest visible to Flatpak browsers
mkdir -p ~/.var/app/&amp;lt;browser-app-id&amp;gt;/config/&amp;lt;browser&amp;gt;/NativeMessagingHosts
flatpak run --command=/app/bin/1password-cli com.1password.1Password --setup-browser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typical setup (Vivaldi or Chromium as Flatpak, 1Password as Flatpak):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Allow the 1Password app to talk to host browsers
flatpak override --user --talk-name=com.1password.1Password com.vivaldi.Vivaldi
flatpak override --user --talk-name=com.1password.1Password org.chromium.Chromium
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enable &lt;strong&gt;Browser Integration&lt;/strong&gt; inside the desktop app: &lt;em&gt;Settings → Developer → Allow connection from browser extensions&lt;/em&gt;. The integration check inside the extension&#39;s settings should now flip to green.&lt;/p&gt;
&lt;h2 id=&quot;when-it-still-doesn-t-work&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#when-it-still-doesn-t-work&quot;&gt;#&lt;/a&gt; When it still doesn&#39;t work&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wayland-only browsers.&lt;/strong&gt; A few extensions misbehave under pure Wayland. &lt;code&gt;--ozone-platform-hint=auto&lt;/code&gt; in the browser launch flags often fixes it. For Vivaldi specifically, &lt;a href=&quot;/blog/vivaldi-persistent-flags/&quot;&gt;Persistent CLI flags for Vivaldi on Linux&lt;/a&gt; is where the flag belongs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;App ID mismatch.&lt;/strong&gt; 1Password 8 uses &lt;code&gt;com.1password.1Password&lt;/code&gt;. The 7.x snap manifest used &lt;code&gt;com.onepassword.OnePassword&lt;/code&gt;. Mixing the two silently fails. Check &lt;code&gt;flatpak list | grep -i password&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;com.1password.1Password.Policy&lt;/code&gt; missing.&lt;/strong&gt; Happens if you grabbed the app from a third-party Flathub mirror without the &lt;code&gt;Policy&lt;/code&gt; extension. Reinstall from the official Flathub remote.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PIN/biometrics on the wrong account.&lt;/strong&gt; Only one signed-in account at a time can serve native messaging. Switch primary account in &lt;em&gt;Settings → Accounts&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;. Why Flatpak is the default install path on immutable Fedora.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/rpm-ostree-rebase-cheatsheet/&quot;&gt;rpm-ostree&amp;colon; rebase&amp;comma; pin&amp;comma; rollback&lt;/a&gt;. If reinstalling 1Password meant a base image change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.1password.com/docs/cli/native-messaging/&quot;&gt;1Password — native messaging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.flatpak.org/en/latest/sandbox-permissions.html&quot;&gt;Flatpak sandbox permissions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="linux"/><category term="flatpak"/><category term="1password"/><category term="bazzite"/><category term="browsers"/><category term="troubleshooting"/>
  </entry>
  
  <entry>
    <title>rpm-ostree: rebase, pin, rollback</title>
    <link href="https://daal.cloud/blog/rpm-ostree-rebase-cheatsheet/"/>
    <id>https://daal.cloud/blog/rpm-ostree-rebase-cheatsheet/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>Cheat-sheet for moving between OS images on rpm-ostree systems. What each verb actually does and how not to lose access to a working boot.</summary>
    <content type="html">&lt;p&gt;&lt;code&gt;rpm-ostree&lt;/code&gt; manages the OS image on Fedora&#39;s atomic desktops (&lt;a href=&quot;https://fedoraproject.org/atomic-desktops/silverblue/&quot;&gt;Silverblue&lt;/a&gt;, &lt;a href=&quot;https://fedoraproject.org/atomic-desktops/kinoite/&quot;&gt;Kinoite&lt;/a&gt;, &lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&lt;/a&gt;). A handful of verbs cover 90% of what you need.&lt;/p&gt;
&lt;h2 id=&quot;the-shape&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#the-shape&quot;&gt;#&lt;/a&gt; The shape&lt;/h2&gt;
&lt;p&gt;You always have &lt;strong&gt;two deployments&lt;/strong&gt; at minimum: the one you&#39;re running and the one you&#39;ll roll back to. Each &lt;code&gt;rpm-ostree&lt;/code&gt; operation stages a &lt;em&gt;new&lt;/em&gt; deployment. Nothing changes until you reboot.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ rpm-ostree status -v
State: idle
Deployments:
* ostree-image-signed:docker://ghcr.io/ublue-os/bazzite:stable
                   Digest: sha256:…
                  Version: 41.20260420.0   (current)
  ostree-image-signed:docker://ghcr.io/ublue-os/bazzite:stable
                   Digest: sha256:…
                  Version: 41.20260413.0   (rollback target)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt; marks &amp;quot;currently booted&amp;quot;. Reboot into the older deployment from GRUB to roll back.&lt;/p&gt;
&lt;h2 id=&quot;verbs&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#verbs&quot;&gt;#&lt;/a&gt; Verbs&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rpm-ostree upgrade&lt;/code&gt;. Fetch the latest ref of the same image. Reboot to apply.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm-ostree rollback&lt;/code&gt;. Make the previous deployment the default for next boot.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm-ostree rebase ostree-image-signed:docker://ghcr.io/&amp;lt;org&amp;gt;/&amp;lt;image&amp;gt;:&amp;lt;tag&amp;gt;&lt;/code&gt;. Switch to a different OS image. How you go between Bazzite GNOME and Bazzite KDE, between &lt;code&gt;:stable&lt;/code&gt; and &lt;code&gt;:testing&lt;/code&gt;, or to a &lt;a href=&quot;https://universal-blue.org/&quot;&gt;uBlue&lt;/a&gt; variant.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm-ostree deploy &amp;lt;version&amp;gt;&lt;/code&gt;. Pin to a specific image version. Subsequent &lt;code&gt;upgrade&lt;/code&gt; calls won&#39;t move past it until you &lt;code&gt;deploy --reset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm-ostree reset&lt;/code&gt;. Drop all layered packages and overrides. Useful when an upgrade is wedged because of a layering conflict.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;pin-a-known-good-deployment&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#pin-a-known-good-deployment&quot;&gt;#&lt;/a&gt; Pin a known-good deployment&lt;/h2&gt;
&lt;p&gt;Pin the current deployment so a future rollback can reach it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo ostree admin pin 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pinned deployments aren&#39;t garbage-collected when newer ones land. List with &lt;code&gt;ostree admin status&lt;/code&gt;. Unpin via &lt;code&gt;ostree admin pin --unpin &amp;lt;index&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;layering-escape-hatches&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#layering-escape-hatches&quot;&gt;#&lt;/a&gt; Layering escape hatches&lt;/h2&gt;
&lt;p&gt;You can install RPMs into the image with &lt;code&gt;rpm-ostree install &amp;lt;pkg&amp;gt;&lt;/code&gt;. They become &lt;strong&gt;layers&lt;/strong&gt; on top of the base. Keep this list short:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every layer has to be re-applied on each rebase, slowing rebase and upgrade.&lt;/li&gt;
&lt;li&gt;A package that conflicts with the new base wedges the upgrade until you &lt;code&gt;rpm-ostree override remove&lt;/code&gt; or &lt;code&gt;rpm-ostree reset&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Layered drivers and kernel modules are particularly fragile across major version jumps.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;when-a-boot-fails&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#when-a-boot-fails&quot;&gt;#&lt;/a&gt; When a boot fails&lt;/h2&gt;
&lt;p&gt;GRUB shows the previous deployment as a separate entry. Reboot, pick one. The next &lt;code&gt;rpm-ostree status&lt;/code&gt; will show the broken one as &amp;quot;Deployment 1&amp;quot;. &lt;code&gt;rpm-ostree rollback&lt;/code&gt; makes it the default; &lt;code&gt;rpm-ostree cleanup -p&lt;/code&gt; drops it once you&#39;ve recovered.&lt;/p&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;. Context for why this is even a thing.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/flatpak-1password-browser-bridge/&quot;&gt;Making 1Password browser extensions talk to the Flatpak desktop app&lt;/a&gt;. Flatpak troubleshooting for sandbox-aware apps.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://coreos.github.io/rpm-ostree/&quot;&gt;rpm-ostree user manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.fedoraproject.org/en-US/fedora-silverblue/updates/&quot;&gt;Fedora image-based updates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="linux"/><category term="bazzite"/><category term="immutable-os"/><category term="rpm-ostree"/><category term="fedora"/>
  </entry>
  
  <entry>
    <title>Persistent CLI flags for Vivaldi on Linux</title>
    <link href="https://daal.cloud/blog/vivaldi-persistent-flags/"/>
    <id>https://daal.cloud/blog/vivaldi-persistent-flags/</id>
    <published>2026-04-25T00:00:00.000Z</published>
    <updated>2026-04-25T00:00:00.000Z</updated>
    <summary>Vivaldi&#39;s launcher reads extra command-line flags from a config file in your home directory on every launch. That&#39;s where `--disable-gpu-compositing` and friends belong. Don&#39;t edit the .desktop file.</summary>
    <content type="html">&lt;p&gt;Vivaldi&#39;s launcher script (&lt;code&gt;/opt/vivaldi/vivaldi&lt;/code&gt;, with &lt;code&gt;/usr/bin/vivaldi-stable&lt;/code&gt; symlinked to it) reads extra CLI flags from a config file in &lt;code&gt;~/.config/&lt;/code&gt; on every launch. That&#39;s the right place for &lt;code&gt;--disable-gpu-compositing&lt;/code&gt;, &lt;code&gt;--enable-features=...&lt;/code&gt;, &lt;code&gt;--ozone-platform-hint=auto&lt;/code&gt;, and other flags you want Vivaldi to use.&lt;/p&gt;
&lt;h2 id=&quot;where-to-write-it&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#where-to-write-it&quot;&gt;#&lt;/a&gt; Where to write it&lt;/h2&gt;
&lt;p&gt;Config filename comes from the channel:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stable:&lt;/strong&gt; &lt;code&gt;~/.config/vivaldi-stable.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshot:&lt;/strong&gt; &lt;code&gt;~/.config/vivaldi-snapshot.conf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One flag per line. (&lt;code&gt;#&lt;/code&gt; is a comment). Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/vivaldi-stable.conf
--disable-gpu-compositing
--ozone-platform-hint=auto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Quit and relaunch Vivaldi for changes to take effect. Closing the last window doesn&#39;t quit Vivaldi if you have a tray icon enabled.&lt;/p&gt;
&lt;h2 id=&quot;confirm-the-flags-are-active&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#confirm-the-flags-are-active&quot;&gt;#&lt;/a&gt; Confirm the flags are active&lt;/h2&gt;
&lt;p&gt;Open &lt;code&gt;vivaldi://version&lt;/code&gt; and check the &lt;strong&gt;Command Line&lt;/strong&gt; field. Every flag you set should be visible there alongside the defaults. Missing flag usually means a typo or a syntax issue (forgetting the leading &lt;code&gt;--&lt;/code&gt; is a common one).&lt;/p&gt;
&lt;h2 id=&quot;why-not-the-desktop-file&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#why-not-the-desktop-file&quot;&gt;#&lt;/a&gt; Why not the .desktop file&lt;/h2&gt;
&lt;p&gt;It&#39;s tempting to edit &lt;code&gt;/usr/share/applications/vivaldi-stable.desktop&lt;/code&gt; and append to &lt;code&gt;Exec=&lt;/code&gt;. Don&#39;t:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Package manager rewrites that file on every Vivaldi update. Your changes drop silently.&lt;/li&gt;
&lt;li&gt;Only affects launches from the desktop application menu, not the CLI or other launchers.&lt;/li&gt;
&lt;li&gt;The launcher reads the conf file &lt;em&gt;in addition to&lt;/em&gt; &lt;code&gt;Exec=&lt;/code&gt;, so you&#39;d still need the conf file for command-line launches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you genuinely need a per-application override (different flags for a launcher icon vs. a CLI alias), copy the .desktop file to &lt;code&gt;~/.local/share/applications/&lt;/code&gt; first and edit the copy.&lt;/p&gt;
&lt;h2 id=&quot;when-something-breaks&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#when-something-breaks&quot;&gt;#&lt;/a&gt; When something breaks&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPU glitches on Wayland.&lt;/strong&gt; &lt;code&gt;--disable-gpu-compositing&lt;/code&gt; usually fixes things, but expect higher CPU load since the CPU now handles compositing. &lt;code&gt;--ozone-platform-hint=auto&lt;/code&gt; can also help.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;vivaldi://flags&lt;/code&gt; overrides.&lt;/strong&gt; Those persist in the user profile, separate from CLI flags. If a flag from &lt;code&gt;~/.config/vivaldi-stable.conf&lt;/code&gt; doesn&#39;t seem to take effect, check &lt;code&gt;vivaldi://flags&lt;/code&gt; for a conflicting override.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vivaldi as Flatpak.&lt;/strong&gt; Conf file path is sandboxed to &lt;code&gt;~/.var/app/com.vivaldi.Vivaldi/config/vivaldi-stable.conf&lt;/code&gt;. Same syntax. Useful when Vivaldi is installed via Flatpak (e.g. on &lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;related&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#related&quot;&gt;#&lt;/a&gt; Related&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/bazzite-overview/&quot;&gt;Bazzite&amp;colon; an immutable gaming-first Fedora variant&lt;/a&gt;. Flatpak browsers on immutable Fedora.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/flatpak-1password-browser-bridge/&quot;&gt;Making 1Password browser extensions talk to the Flatpak desktop app&lt;/a&gt;. When the password manager extension can&#39;t talk to the desktop app.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;references&quot; tabindex=&quot;-1&quot;&gt;&lt;a class=&quot;header-anchor&quot; href=&quot;#references&quot;&gt;#&lt;/a&gt; References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://help.vivaldi.com/desktop/install-update/command-line-switches/&quot;&gt;Vivaldi help: command-line switches&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://peter.sh/experiments/chromium-command-line-switches/&quot;&gt;Chromium command-line switches list&lt;/a&gt;. Vivaldi accepts all of these.&lt;/li&gt;
&lt;/ul&gt;
</content>
    <category term="linux"/><category term="vivaldi"/><category term="browsers"/><category term="desktop"/><category term="troubleshooting"/>
  </entry>
  
</feed>
