<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Open-Source | Bas Nijholt</title><link>https://www.nijho.lt/tag/open-source/</link><atom:link href="https://www.nijho.lt/tag/open-source/index.xml" rel="self" type="application/rss+xml"/><description>Open-Source</description><generator>Hugo</generator><language>en-us</language><copyright>© 2026</copyright><lastBuildDate>Mon, 19 Oct 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.nijho.lt/media/icon_hu_408a8f6cef43e327.png</url><title>Open-Source</title><link>https://www.nijho.lt/tag/open-source/</link></image><item><title>Roll your own, fork the rest 🍴</title><link>https://www.nijho.lt/post/roll-your-own-fork-the-rest/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/roll-your-own-fork-the-rest/</guid><description>&lt;h2 id="introduction-a-follow-up-to-something-i-wrote"&gt;Introduction: a follow-up to something I wrote &lt;a class="anchor" href="#introduction-a-follow-up-to-something-i-wrote" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In April 2025 I published &lt;a href="https://www.nijho.lt/post/dependencies/"&gt;Confessions of an asymmetric hypocrite&lt;/a&gt;, about my split personality on dependencies.&#10;Strict minimalism for the libraries I publish, and then, for applications:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;For &lt;em&gt;applications&lt;/em&gt; (especially CLIs in isolated environments), I&amp;rsquo;m far more liberal with dependencies to build features quickly.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;I also wrote that I &amp;ldquo;strongly prefer dependencies that are already widely adopted and trusted by the community&amp;rdquo; because &amp;ldquo;these are less likely to cause unexpected issues&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Then I spent a year building &lt;a href="https://www.nijho.lt/post/mindroom/"&gt;MindRoom&lt;/a&gt;, which is an application, exactly the category where I said I could relax.&lt;/p&gt;&#10;&lt;p&gt;I want to report back.&#10;&amp;ldquo;Prefer &lt;span class="hover-tip" data-tooltip="Code that so many people already run in production that the common bugs have been found and fixed before you get there." tabindex="0"&gt;battle-tested&lt;/span&gt; dependencies&amp;rdquo; was the right rule.&#10;I just had the wrong idea of what battle-tested means.&#10;I thought a project with tens of thousands of GitHub stars qualified.&#10;Now I think it means &lt;strong&gt;millions of downloads, every day.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#introduction-a-follow-up-to-something-i-wrote"&gt;Introduction: a follow-up to something I wrote&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#1-stars-measure-interest-not-usage"&gt;1. Stars measure interest, not usage&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-a-popular-protocol-is-not-a-popular-library"&gt;2. A popular protocol is not a popular library&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-i-checked-the-rule-against-my-own-code"&gt;3. I checked the rule against my own code&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-roll-your-own-the-library-gives-you-the-easy-10"&gt;4. Roll your own: the library gives you the easy 10%&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-fork-your-own-when-the-problem-is-hard"&gt;5. Fork your own: when the problem is hard&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#agno-patches-instead-of-a-fork"&gt;Agno: patches instead of a fork&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-i-am-not-dunking-on-open-source-including-mine"&gt;6. I am not dunking on open source, including mine&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-my-rule-for-the-next-uv-add"&gt;7. My rule for the next &lt;code&gt;uv add&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Stars measure interest, not usage. For every dependency, ask how many projects actually run the code you are about to rely on, and how much you rely on it.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Millions of downloads a day?&lt;/strong&gt; Depend on it.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Fewer, and it only gives you the easy part?&lt;/strong&gt; Roll your own. The hard part was going to be yours anyway.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Fewer, and hard to build?&lt;/strong&gt; Fork it, and treat the fork as yours.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If I started MindRoom over, I would write my own HTTP requests to the model providers instead of using an AI framework.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;strong&gt;Update 2026-10-31:&lt;/strong&gt; In the two weeks after publishing, I took my own advice twice.&#10;MindRoom&amp;rsquo;s knowledge search no longer uses ChromaDB, and code MindRoom owns has replaced Agno&amp;rsquo;s runtime.&#10;Agno is now only MindRoom&amp;rsquo;s tool library.&#10;The full stories are in &lt;a href="#comments"&gt;the comments&lt;/a&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="1-stars-measure-interest-not-usage"&gt;1. Stars measure interest, not usage &lt;a class="anchor" href="#1-stars-measure-interest-not-usage" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When I picked &lt;a href="https://github.com/agno-agi/agno" target="_blank" rel="noopener"&gt;Agno&lt;/a&gt; as MindRoom&amp;rsquo;s &lt;span class="hover-tip" data-tooltip="A library that wraps the model providers&amp;#39; APIs and adds agents, tools, memory, knowledge bases and session storage on top." tabindex="0"&gt;AI framework&lt;/span&gt;, it looked like the safe choice.&#10;Today it has over 42,000 stars and 6,000 forks.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;It also gets about &lt;strong&gt;78,000 downloads a day&lt;/strong&gt; on PyPI.&#10;&lt;a href="https://github.com/encode/httpx" target="_blank" rel="noopener"&gt;httpx&lt;/a&gt;, one of Python&amp;rsquo;s standard HTTP libraries, gets about &lt;strong&gt;25 million&lt;/strong&gt;.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;A star is something you click once, when a README looks exciting.&#10;A download happens every time some project installs the package.&lt;/p&gt;&#10;&lt;p&gt;Most of those downloads are &lt;span class="hover-tip" data-tooltip="Continuous integration: test and build pipelines that install a fresh environment on every push, often many times a day per project." tabindex="0"&gt;CI&lt;/span&gt;, not people, so downloads are not users either.&#10;But a package with millions of downloads a day is installed by a huge number of different projects.&#10;Many of them use the same functions you are about to use, so they hit the bugs before you do.&lt;/p&gt;&#10;&lt;p&gt;Battle-tested is also not the same as old.&#10;I use &lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener"&gt;uv&lt;/a&gt; for everything, and it has only been around for a &lt;span class="hover-tip" data-tooltip="Astral released the first version of uv in February 2024." tabindex="0"&gt;couple of years&lt;/span&gt;.&#10;I run into bugs in uv too, but there is simply no better alternative, and it gets about 3.6 million downloads a day.&lt;/p&gt;&#10;&lt;h2 id="2-a-popular-protocol-is-not-a-popular-library"&gt;2. A popular protocol is not a popular library &lt;a class="anchor" href="#2-a-popular-protocol-is-not-a-popular-library" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The other place my old idea failed was &lt;a href="https://matrix.org" target="_blank" rel="noopener"&gt;Matrix&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Building on Matrix felt obviously correct.&#10;An open protocol, about a decade old, with serious security reviews from &lt;span class="hover-tip" data-tooltip="Governments run their own Matrix deployments, for example Tchap for the French civil service and BwMessenger for the German armed forces." tabindex="0"&gt;large organizations and governments&lt;/span&gt;.&#10;Millions of people use it.&lt;/p&gt;&#10;&lt;p&gt;Matrix is still a good protocol and I am still on it.&#10;But even though millions of people use Matrix, there are only a few client libraries.&lt;/p&gt;&#10;&lt;p&gt;MindRoom streams agent replies by editing the same message many times a second.&#10;Almost every part of the ecosystem I touched had a problem with that: &lt;span class="hover-tip" data-tooltip="The most widely used Matrix app, made by the company behind the protocol." tabindex="0"&gt;Element&lt;/span&gt;, &lt;span class="hover-tip" data-tooltip="A Matrix web client focused on a simple, Discord-like interface. MindRoom&amp;#39;s chat app is a fork of it." tabindex="0"&gt;Cinny&lt;/span&gt;, &lt;span class="hover-tip" data-tooltip="The Python Matrix client library MindRoom&amp;#39;s agents use to log in, read and send messages." tabindex="0"&gt;matrix-nio&lt;/span&gt;, and the &lt;span class="hover-tip" data-tooltip="matrix-js-sdk, the library Element Web and Cinny are built on." tabindex="0"&gt;Matrix JavaScript SDK&lt;/span&gt;.&#10;In the JavaScript SDK, edits that arrived while a thread was still loading were silently dropped, so you saw stale text until you reloaded.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;What counts is how many projects use the library you build on.&#10;Element&amp;rsquo;s users do not test matrix-js-sdk for you. Other people building on matrix-js-sdk do.&#10;It gets roughly 100,000 downloads a day. matrix-nio, the Python client, about 23,000.&lt;/p&gt;&#10;&lt;h2 id="3-i-checked-the-rule-against-my-own-code"&gt;3. I checked the rule against my own code &lt;a class="anchor" href="#3-i-checked-the-rule-against-my-own-code" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I pulled the daily download numbers for the packages I use and looked for workarounds, patches and forks in my own source.&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Package&lt;/th&gt;&#10;&lt;th&gt;Downloads per day&lt;/th&gt;&#10;&lt;th&gt;What it cost me&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;requests&lt;/td&gt;&#10;&lt;td&gt;49M&lt;/td&gt;&#10;&lt;td&gt;nothing I had to work around&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;pydantic&lt;/td&gt;&#10;&lt;td&gt;35M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;numpy&lt;/td&gt;&#10;&lt;td&gt;33M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;httpx&lt;/td&gt;&#10;&lt;td&gt;25M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;aiohttp&lt;/td&gt;&#10;&lt;td&gt;18M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;FastAPI&lt;/td&gt;&#10;&lt;td&gt;17M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;chromadb&lt;/td&gt;&#10;&lt;td&gt;270K&lt;/td&gt;&#10;&lt;td&gt;workarounds for a bug that holds the GIL&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;matrix-js-sdk&lt;/td&gt;&#10;&lt;td&gt;~100K&lt;/td&gt;&#10;&lt;td&gt;patched&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;mem0ai&lt;/td&gt;&#10;&lt;td&gt;79K&lt;/td&gt;&#10;&lt;td&gt;largely reimplemented&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;agno&lt;/td&gt;&#10;&lt;td&gt;78K&lt;/td&gt;&#10;&lt;td&gt;&lt;strong&gt;126 documented workarounds&lt;/strong&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;matrix-nio&lt;/td&gt;&#10;&lt;td&gt;23K&lt;/td&gt;&#10;&lt;td&gt;forked&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Above a million a day, I had nothing to work around.&#10;Below it, every package on this list cost me a patch, a fork, a rewrite, or a pile of workarounds.&lt;/p&gt;&#10;&lt;p&gt;Take &lt;a href="https://github.com/chroma-core/chroma" target="_blank" rel="noopener"&gt;ChromaDB&lt;/a&gt;, the vector database behind MindRoom&amp;rsquo;s knowledge bases.&#10;In some cases it holds Python&amp;rsquo;s &lt;span class="hover-tip" data-tooltip="The Global Interpreter Lock: only one thread can run Python code at a time. Native code is supposed to release it while it waits, so other threads can keep going." tabindex="0"&gt;GIL&lt;/span&gt; during I/O, which blocks every other thread in the process.&#10;I opened &lt;a href="https://github.com/chroma-core/chroma/pull/7692" target="_blank" rel="noopener"&gt;a pull request&lt;/a&gt; to fix it, and nobody has looked at it yet.&#10;Instead of forking, I run every knowledge read in a short-lived child process, because a process can be killed and a blocked thread cannot.&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;My fork of matrix-nio has about 8,900 lines added and 2,000 removed in the library code since the last upstream release.&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;This does not mean every small package needs replacing.&#10;MindRoom pulls in a few hundred packages in total, and plenty of them have modest download numbers.&#10;The scrutiny should match how much I depend on a package.&#10;Something I use for one small, specific purpose is fine.&#10;Agno and matrix-nio are the base that MindRoom is built on, which is probably also why they hurt the most.&lt;/p&gt;&#10;&lt;h2 id="4-roll-your-own-the-library-gives-you-the-easy-10"&gt;4. Roll your own: the library gives you the easy 10% &lt;a class="anchor" href="#4-roll-your-own-the-library-gives-you-the-easy-10" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Take memory libraries, &lt;a href="https://github.com/mem0ai/mem0" target="_blank" rel="noopener"&gt;Mem0&lt;/a&gt; and friends.&#10;I have &lt;a href="https://www.nijho.lt/post/file-based-rag-memory/"&gt;already written about this&lt;/a&gt;: I cloned LlamaIndex, LangChain, Letta, Mem0 and PydanticAI, studied how they work, and re-implemented the parts I needed. My memory system ended up &amp;ldquo;mostly modeled after Mem0&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;The core idea is not hard.&#10;With &lt;span class="hover-tip" data-tooltip="Retrieval-augmented generation: look up relevant text first and paste it into the prompt, so the model answers from your documents instead of from memory." tabindex="0"&gt;RAG&lt;/span&gt;, you chunk, embed, retrieve, re-rank and reconcile. Once you have read a couple of implementations you can write it, and afterwards you understand every line.&lt;/p&gt;&#10;&lt;p&gt;The same is true of an agent framework.&#10;It builds a JSON payload, posts it to an HTTP endpoint, parses a &lt;span class="hover-tip" data-tooltip="Providers send the reply as server-sent events: a long-lived HTTP response with one small JSON chunk per token or tool-call fragment." tabindex="0"&gt;streaming response&lt;/span&gt;, keeps a list of messages, and loops when the model asks for a tool call.&#10;With AI, that is a day or two of work.&lt;/p&gt;&#10;&lt;p&gt;The hard part is making it reliable at scale: never blocking the &lt;span class="hover-tip" data-tooltip="In async Python, one thread runs all tasks by switching between them. A single call that blocks freezes every conversation at once." tabindex="0"&gt;event loop&lt;/span&gt;, and handling the crazy edge cases.&#10;With an agent system you end up building a very complicated state machine, and if you do not watch it, any combination of states is possible. Almost all of them are edge cases.&lt;/p&gt;&#10;&lt;p&gt;A library gives you the easy 10% and leaves you the hard 90%.&#10;And you do that 90% inside somebody else&amp;rsquo;s code, where you cannot fix the root cause.&lt;/p&gt;&#10;&lt;p&gt;MindRoom is now about 360,000 lines of Python, plus about 750,000 lines of tests.&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;&#10;A good part of that is code I wrote instead of depending on a library:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Memory and knowledge:&lt;/strong&gt; about 15,600 lines across 50 files.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Conversation history and &lt;span class="hover-tip" data-tooltip="Summarizing older turns of a conversation so a long thread still fits in the model&amp;#39;s context window." tabindex="0"&gt;compaction&lt;/span&gt;:&lt;/strong&gt; about 8,400 lines.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;An OpenAI-compatible API:&lt;/strong&gt; about 2,300 lines, so MindRoom agents show up as selectable &amp;ldquo;models&amp;rdquo; in any chat frontend that speaks the OpenAI protocol.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude provider support:&lt;/strong&gt; about 3,000 lines.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If I have to write the hard 90% either way, I might as well write the easy 10% too.&lt;/p&gt;&#10;&lt;h2 id="5-fork-your-own-when-the-problem-is-hard"&gt;5. Fork your own: when the problem is hard &lt;a class="anchor" href="#5-fork-your-own-when-the-problem-is-hard" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I am not going to write my own &lt;span class="hover-tip" data-tooltip="Matrix encryption means per-device keys, the Olm and Megolm ratchets, cross-signing, device verification and key backup, all of which have to interoperate with every other client." tabindex="0"&gt;end-to-end encrypted&lt;/span&gt; Matrix SDK, or my own &lt;span class="hover-tip" data-tooltip="The server that stores your rooms and syncs them with other Matrix servers, like a mail server for chat." tabindex="0"&gt;homeserver&lt;/span&gt;.&#10;Those are hard, and also far below the line.&#10;So there I fork.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I forked the Python Matrix client.&lt;/strong&gt; MindRoom ships its own fork of matrix-nio rather than the upstream package.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I forked the Matrix server, twice.&lt;/strong&gt; First &lt;span class="hover-tip" data-tooltip="The original Matrix homeserver, written in Python by the Matrix.org team." tabindex="0"&gt;Synapse&lt;/span&gt;. When I ran into scaling problems I moved to &lt;a href="https://github.com/matrix-construct/tuwunel" target="_blank" rel="noopener"&gt;Tuwunel&lt;/a&gt; and forked that.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I forked the web client.&lt;/strong&gt; &lt;a href="https://github.com/cinnyapp/cinny" target="_blank" rel="noopener"&gt;Cinny&lt;/a&gt; is the Matrix web client MindRoom uses.&lt;/p&gt;&#10;&lt;p&gt;None of these are AI projects. AI is just where I felt it most.&lt;/p&gt;&#10;&lt;p&gt;Whether a fork stays a fork depends on the maintainers.&#10;Tuwunel is extremely receptive to my patches, so I upstream anything that is general enough, and over the last six months I am its fourth most active contributor by commits.&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt;&#10;Cinny has not accepted any of my pull requests, so I forked it and stopped trying to upstream.&#10;Same with &lt;span class="hover-tip" data-tooltip="An open-source, self-hosted ChatGPT-style web interface that can talk to many model providers." tabindex="0"&gt;LibreChat&lt;/span&gt;: I had patches open for over a year without a reply, so I forked.&lt;/p&gt;&#10;&lt;p&gt;Keeping a fork up to date used to be miserable.&#10;With an LLM doing the rebasing, pulling in the latest upstream is now cheap, though still a bit of a risk every time.&#10;The bigger cost is that once you fork, you are responsible for the whole codebase, including the parts you never changed.&lt;/p&gt;&#10;&lt;h3 id="agno-patches-instead-of-a-fork"&gt;Agno: patches instead of a fork &lt;a class="anchor" href="#agno-patches-instead-of-a-fork" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I did not fork Agno. I &lt;span class="hover-tip" data-tooltip="Replacing a library&amp;#39;s functions or methods at runtime from your own code, without changing the installed package." tabindex="0"&gt;monkey-patched&lt;/span&gt; it.&#10;Agno is very actively developed, I have talked to the maintainer, and they know about these problems, so I hoped to upstream my fixes.&#10;I have filed 33 issues and pull requests there.&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;That got out of hand.&#10;MindRoom now carries &lt;strong&gt;126 documented workarounds&lt;/strong&gt; for Agno across 59 files.&#10;Most of them live in 36 dedicated compatibility modules, about 7,200 lines of code, with the rest patched inline in 23 other files.&#10;Every one uses the same comment format, so I can &lt;a href="https://github.com/search?q=repo%3Amindroom-ai%2Fmindroom&amp;#43;AGNO_COMPAT&amp;amp;type=code" target="_blank" rel="noopener"&gt;find all of them with one search&lt;/a&gt;.&#10;Each one says what is wrong upstream, links the issue or PR, and says when I can delete it.&lt;sup id="fnref:10"&gt;&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref"&gt;10&lt;/a&gt;&lt;/sup&gt;&#10;A few examples:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Search failures become plausible empty results.&lt;/strong&gt; A failed knowledge search and an empty index return the same empty list, so my agents confidently told me things did not exist.&lt;sup id="fnref:11"&gt;&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref"&gt;11&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Streamed tool calls get malformed or merged.&lt;/strong&gt;&lt;sup id="fnref:12"&gt;&lt;a href="#fn:12" class="footnote-ref" role="doc-noteref"&gt;12&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reasoning is silently lost when tool calls are replayed.&lt;/strong&gt;&lt;sup id="fnref:13"&gt;&lt;a href="#fn:13" class="footnote-ref" role="doc-noteref"&gt;13&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Provider errors collapse into a generic 502&lt;/strong&gt;, which throws away the error code you need to decide whether to retry.&lt;sup id="fnref:14"&gt;&lt;a href="#fn:14" class="footnote-ref" role="doc-noteref"&gt;14&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;At this size, the patches amount to a fork.&lt;/p&gt;&#10;&lt;h2 id="6-i-am-not-dunking-on-open-source-including-mine"&gt;6. I am not dunking on open source, including mine &lt;a class="anchor" href="#6-i-am-not-dunking-on-open-source-including-mine" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Everything I named is open source I use every day, for free, built by people doing their best.&#10;I filed issues and PRs, and where maintainers merged fixes I deleted my workarounds.&lt;/p&gt;&#10;&lt;p&gt;My own software is in the same category.&#10;Most new AI tooling is not very good, and MindRoom may well be included.&#10;It is large, fast-moving, and nowhere near millions of daily downloads.&#10;By my own rule, you should think twice before depending on it.&lt;/p&gt;&#10;&lt;p&gt;I also have to admit that I have not yet built a project the roll-your-own way from the start.&#10;So comparing &amp;ldquo;what I would do next time&amp;rdquo; with what I actually did is not entirely fair.&#10;This is what I think right now, and a year from now I may have to write another post correcting this one.&lt;/p&gt;&#10;&lt;h2 id="7-my-rule-for-the-next-uv-add"&gt;7. My rule for the next &lt;code&gt;uv add&lt;/code&gt; &lt;a class="anchor" href="#7-my-rule-for-the-next-uv-add" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I weigh three questions.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;How many projects use it?&lt;/strong&gt;&#10;Daily downloads of the actual package, not stars, and not how many people use the product built on top.&#10;In the millions: depend on it.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;How much do I rely on it?&lt;/strong&gt;&#10;Agno and matrix-nio are the base of MindRoom.&#10;A library I use in a CLI to format a question is not, and if I use it in a narrow way I do not need my own version.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;How hard is it, and how receptive is upstream?&lt;/strong&gt;&#10;If the library is something AI can write in a couple of days, roll my own.&#10;If it is hard, fork it.&#10;If the maintainers accept patches, upstream what is general enough.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;If I started MindRoom over today, I would write the HTTP requests to the model providers myself.&lt;/strong&gt;&#10;Not even the OpenAI or Anthropic SDKs.&#10;And even httpx is less safe than its download numbers suggest: in early 2026 its maintainer closed the issue tracker and discussions, and the latest stable release is from December 2024.&lt;sup id="fnref:15"&gt;&lt;a href="#fn:15" class="footnote-ref" role="doc-noteref"&gt;15&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;In 2025 I wrote that the right answer is &amp;ldquo;it depends on the situation&amp;rdquo;, and I still believe that.&#10;I just had the wrong threshold.&#10;Battle-tested means millions of downloads a day.&#10;Below that: roll your own, and fork the rest.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;From the GitHub API (&lt;code&gt;api.github.com/repos/agno-agi/agno&lt;/code&gt;) on 2026-10-02: 42,385 stars and 6,041 forks.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;PyPI numbers are the &lt;code&gt;last_day&lt;/code&gt; field of the &lt;a href="https://pypistats.org" target="_blank" rel="noopener"&gt;pypistats.org&lt;/a&gt; API (&lt;code&gt;/api/packages/&amp;lt;name&amp;gt;/recent&lt;/code&gt;), fetched on 2026-10-02, except uv, fetched on 2026-10-18 (3,648,827). The matrix-js-sdk number is npm&amp;rsquo;s weekly download count (about 730,000) divided by seven. Daily numbers wobble, so treat them as orders of magnitude.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;When events arrive for a thread that has not finished initializing, the SDK&amp;rsquo;s replay path never applies them, so an &lt;code&gt;m.replace&lt;/code&gt; edit (the event type Matrix uses for &amp;ldquo;this replaces message X&amp;rdquo;) is stored and never shown. I replayed a captured event trace through the SDK: unpatched, zero of seventeen edits reached the listeners; patched, all seventeen. MindRoom carries the fix as a &lt;code&gt;patch-package&lt;/code&gt; patch on the SDK&amp;rsquo;s &lt;code&gt;thread.ts&lt;/code&gt;.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;For each direct dependency I looked at three things in the MindRoom repository: whether &lt;code&gt;pyproject.toml&lt;/code&gt; pins a fork instead of the upstream package, whether &lt;code&gt;patch-package&lt;/code&gt; patches it, and whether the source has workaround markers or comments pointing at upstream issues for it. The packages above a million downloads a day had none of those.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;The PR is titled &amp;ldquo;Release GIL during metadata I/O&amp;rdquo;. On the MindRoom side, &lt;code&gt;knowledge/read_process.py&lt;/code&gt; starts the child process, and a TODO says to remove it once a Chroma release with the fix is pinned. A blocked thread holding the GIL cannot be interrupted from Python, but the kernel will always terminate a child process.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:6"&gt;&#10;&lt;p&gt;Compared with upstream matrix-nio 0.25.2 (released 2024-10-04): &lt;code&gt;git diff --shortstat&lt;/code&gt; on the library code gives 8,872 lines added and 1,966 removed across 52 files, from 89 commits, 76 of them mine. The fork is published on PyPI as &lt;code&gt;mindroom-nio&lt;/code&gt;.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:7"&gt;&#10;&lt;p&gt;Counted with &lt;code&gt;git ls-files&lt;/code&gt; and &lt;code&gt;wc -l&lt;/code&gt; on the MindRoom repository on 2026-10-18: 360,513 lines in 1,222 Python files under &lt;code&gt;src/&lt;/code&gt;, 750,657 lines under &lt;code&gt;tests/&lt;/code&gt;, and another 81,543 lines of TypeScript in the frontend. The per-feature numbers below come from the same count over the relevant directories.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:8"&gt;&#10;&lt;p&gt;Non-merge commits on Tuwunel&amp;rsquo;s main branch since 2026-04-18: Jason Volk 1,332, June Strawberry 325, dasha_uwu 55, me 42.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:9"&gt;&#10;&lt;p&gt;Issues and pull requests in &lt;a href="https://github.com/agno-agi/agno/issues?q=author%3Abasnijholt" target="_blank" rel="noopener"&gt;agno-agi/agno&lt;/a&gt; with me as the author, counted on 2026-10-02.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:10"&gt;&#10;&lt;p&gt;Every workaround starts with a comment like &lt;code&gt;# AGNO_COMPAT: Knowledge search hides provider failures as empty results.&lt;/code&gt;, followed by &lt;code&gt;Reason:&lt;/code&gt;, &lt;code&gt;Upstream issue:&lt;/code&gt;, &lt;code&gt;Upstream PR:&lt;/code&gt;, &lt;code&gt;Remove when:&lt;/code&gt; and the tests that cover it. The counts come from &lt;code&gt;grep&lt;/code&gt; over &lt;code&gt;src/&lt;/code&gt;: 126 &lt;code&gt;AGNO_COMPAT:&lt;/code&gt; markers in 59 files, and 7,160 lines in the 36 &lt;code&gt;agno_compat_*&lt;/code&gt; modules.&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:11"&gt;&#10;&lt;p&gt;&lt;code&gt;Knowledge.search&lt;/code&gt; catches vector store and provider errors and returns &lt;code&gt;[]&lt;/code&gt;. &lt;a href="https://github.com/agno-agi/agno/issues/10150" target="_blank" rel="noopener"&gt;agno#10150&lt;/a&gt;, partially fixed by &lt;a href="https://github.com/agno-agi/agno/pull/10152" target="_blank" rel="noopener"&gt;agno#10152&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:12"&gt;&#10;&lt;p&gt;When a streamed tool-call index does not start at zero, Agno leaves empty placeholder calls. &lt;a href="https://github.com/agno-agi/agno/issues/8879" target="_blank" rel="noopener"&gt;agno#8879&lt;/a&gt;, fix proposed in &lt;a href="https://github.com/agno-agi/agno/pull/8880" target="_blank" rel="noopener"&gt;agno#8880&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:12" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:13"&gt;&#10;&lt;p&gt;With the OpenAI Responses API, Agno keeps only the last reasoning item, so it cannot be replayed with the tool calls it belonged to. &lt;a href="https://github.com/agno-agi/agno/issues/9960" target="_blank" rel="noopener"&gt;agno#9960&lt;/a&gt;; partial fixes landed in &lt;a href="https://github.com/agno-agi/agno/pull/10075" target="_blank" rel="noopener"&gt;agno#10075&lt;/a&gt; and &lt;a href="https://github.com/agno-agi/agno/pull/10395" target="_blank" rel="noopener"&gt;agno#10395&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:13" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:14"&gt;&#10;&lt;p&gt;Unclassified &lt;code&gt;ModelProviderError&lt;/code&gt; failures default to HTTP 502, so a real network blip and a permanent error look the same. &lt;a href="https://github.com/agno-agi/agno/issues/8869" target="_blank" rel="noopener"&gt;agno#8869&lt;/a&gt;, partially addressed by &lt;a href="https://github.com/agno-agi/agno/pull/8870" target="_blank" rel="noopener"&gt;agno#8870&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:14" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:15"&gt;&#10;&lt;p&gt;The latest stable release on &lt;a href="https://pypi.org/project/httpx/#history" target="_blank" rel="noopener"&gt;PyPI&lt;/a&gt; is 0.28.1, uploaded 2024-12-06. There have been 1.0 development pre-releases since, the latest in August 2026. The closing of issues and discussions was widely discussed in the Python community in March 2026.&amp;#160;&lt;a href="#fnref:15" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Friend-to-friend ZFS backups, version two</title><link>https://www.nijho.lt/post/zfs-tenant/</link><pubDate>Mon, 28 Sep 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/zfs-tenant/</guid><description>&lt;p&gt;Two years ago I wrote about &lt;a href="https://www.nijho.lt/post/truenas-remote-backups/"&gt;friend-to-friend backups with TrueNAS&lt;/a&gt;.&#10;A friend and I store each other&amp;rsquo;s ZFS snapshots, which is the cheapest off-site backup there is.&#10;TrueNAS&amp;rsquo;s replication tasks wanted root on the receiving system, and neither of us wanted to give the other root on the machine that holds our family photos.&#10;So each of us ran a TrueNAS VM on a hypervisor, backed by an iSCSI zvol on the real NAS, and gave the other person root inside that VM instead.&#10;Sounds complicated?&#10;It was.&#10;It worked, but it meant running ZFS on a zvol on ZFS, inside a VM that each of us had to keep updated.&lt;/p&gt;&#10;&lt;p&gt;Since then I &lt;a href="https://www.nijho.lt/post/truenas-to-nixos/"&gt;replaced TrueNAS with NixOS&lt;/a&gt;, and my friend plans to move his machines to NixOS too.&#10;This time we looked for the simplest solution, built from standard Linux and ZFS features wherever possible.&#10;He wanted plain &lt;code&gt;zfs send&lt;/code&gt; and &lt;code&gt;zfs receive&lt;/code&gt;, with his data still encrypted and his keys never on my server.&#10;I agreed, on one condition: neither of us should be able to destroy the other&amp;rsquo;s datasets.&lt;/p&gt;&#10;&lt;p&gt;The result is &lt;a href="https://github.com/basnijholt/zfs-tenant" target="_blank" rel="noopener"&gt;zfs-tenant&lt;/a&gt;, which is now &lt;a href="https://pypi.org/project/zfs-tenant/" target="_blank" rel="noopener"&gt;on PyPI&lt;/a&gt; and has &lt;a href="https://zfs-tenant.nijho.lt" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt;.&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://www.nijho.lt/post/zfs-tenant/logo.svg" width="220" alt="The zfs-tenant logo: a storage pool with two dim compartments for the host and one locked amber compartment for a friend, filled up to a dashed quota line" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#why-zfs"&gt;Why ZFS&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-we-wanted"&gt;What we wanted&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#zfs-already-had-most-of-it"&gt;ZFS already had most of it&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#rehearsing-the-permissions-in-a-vm"&gt;Rehearsing the permissions in a VM&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#a-gate-that-speaks-syncoid"&gt;A gate that speaks syncoid&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#zones-a-second-wall-in-the-kernel"&gt;Zones: a second wall in the kernel&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#who-stops-what"&gt;Who stops what&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-it-cannot-hide"&gt;What it cannot hide&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#setting-it-up"&gt;Setting it up&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#knowing-when-it-stops-working"&gt;Knowing when it stops working&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#restoring"&gt;Restoring&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-first-real-push"&gt;The first real push&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#tested-like-everything-else"&gt;Tested like everything else&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#ai-scope-creep-or-useful"&gt;AI scope creep or useful?&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="why-zfs"&gt;Why ZFS &lt;a class="anchor" href="#why-zfs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://openzfs.org/" target="_blank" rel="noopener"&gt;OpenZFS&lt;/a&gt; is a filesystem and a volume manager in one.&#10;You add disks to a pool, and it behaves like one large partition: every dataset you create draws from the same free space, and each can still have its own quota and encryption.&#10;ZFS checksums every block it writes, so it notices when a disk hands back something else, and with a mirror or RAIDZ it repairs the block from a good copy.&#10;That protection costs disk space: a mirror of two disks holds one disk&amp;rsquo;s worth of data.&lt;/p&gt;&#10;&lt;p&gt;For me, snapshots are one of the coolest parts.&#10;ZFS never overwrites data in place, so a snapshot only has to hold on to the blocks that existed at that moment.&#10;Taking one is instant, and it costs no space until the data changes.&#10;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-send.8.html" target="_blank" rel="noopener"&gt;&lt;code&gt;zfs send&lt;/code&gt;&lt;/a&gt; turns a snapshot, or the difference between two snapshots, into a stream that &lt;code&gt;zfs receive&lt;/code&gt; writes into a pool on another machine, usually piped through SSH.&#10;It moves blocks, not files: an incremental send contains only the blocks that changed, and ZFS knows which ones those are without visiting a single file.&#10;A bajillion one-kilobyte files do not slow it down, because nothing has to look at them one by one.&#10;For comparison, restic on my PC needed &lt;a href="https://www.nijho.lt/post/btrfs-to-zfs/"&gt;an hour and a half per run&lt;/a&gt; just to check close to a hundred million files for changes.&lt;/p&gt;&#10;&lt;p&gt;Two tools from the same project automate this.&#10;&lt;a href="https://github.com/jimsalterjrs/sanoid" target="_blank" rel="noopener"&gt;sanoid&lt;/a&gt; takes snapshots on a schedule and prunes them by policy, for example hourly ones for a day and daily ones for a month.&#10;syncoid wraps &lt;code&gt;zfs send&lt;/code&gt; and &lt;code&gt;zfs receive&lt;/code&gt; over SSH: it finds the newest snapshot both sides share and sends everything after it.&lt;/p&gt;&#10;&lt;h2 id="what-we-wanted"&gt;What we wanted &lt;a class="anchor" href="#what-we-wanted" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;We wrote down the rules before anything else:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;He cannot see any of my data, including metadata like dataset names and sizes.&lt;/li&gt;&#10;&lt;li&gt;He cannot change or destroy anything of mine.&lt;/li&gt;&#10;&lt;li&gt;I cannot read his data, because his keys stay at his house.&lt;/li&gt;&#10;&lt;li&gt;He gets one dataset where he can create and remove nested datasets as he likes.&lt;/li&gt;&#10;&lt;li&gt;That dataset has a size limit.&lt;/li&gt;&#10;&lt;li&gt;No new replication tool on either side: plain &lt;code&gt;zfs send&lt;/code&gt; or syncoid.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Until then his NAS runs TrueNAS, so whatever I built also had to work on a machine without NixOS.&lt;/p&gt;&#10;&lt;h2 id="zfs-already-had-most-of-it"&gt;ZFS already had most of it &lt;a class="anchor" href="#zfs-already-had-most-of-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;OpenZFS has a permission system that most people never touch: &lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-allow.8.html" target="_blank" rel="noopener"&gt;&lt;code&gt;zfs allow&lt;/code&gt;&lt;/a&gt;.&#10;It delegates individual operations like &lt;code&gt;receive&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, and &lt;code&gt;destroy&lt;/code&gt; on one dataset to an unprivileged user.&#10;Put a root-owned &lt;code&gt;quota&lt;/code&gt; on that dataset and the user cannot store more than you agreed.&#10;Raw sends (&lt;code&gt;zfs send -w&lt;/code&gt;) ship the encrypted blocks as they are, so the receiving side never needs the key.&lt;/p&gt;&#10;&lt;p&gt;On paper, that covers five of the six rules.&#10;The gap is visibility.&#10;Any local user can run &lt;code&gt;zfs list&lt;/code&gt; and see every dataset on the machine, with names, sizes, and properties.&#10;And an SSH account for replication is, by default, also a shell.&lt;/p&gt;&#10;&lt;p&gt;There are tools that would sidestep some of this.&#10;&lt;a href="https://zrepl.github.io/" target="_blank" rel="noopener"&gt;zrepl&lt;/a&gt; has a sink mode with a subtree per client, but it replaces sanoid and syncoid on both sides and runs as root on the receiver.&#10;restic or borg to a friend&amp;rsquo;s box would also work, but everything else on my network is ZFS: my machines &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/replication.nix#L40-L49" target="_blank" rel="noopener"&gt;replicate to the NAS with syncoid&lt;/a&gt;, and a restore is &lt;code&gt;zfs send&lt;/code&gt; and &lt;code&gt;zfs receive&lt;/code&gt;.&#10;Adding another backup format means one more thing to understand when something breaks.&#10;My current off-site copy goes to &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/docker-lxc/rclone-b2-backup.nix" target="_blank" rel="noopener"&gt;Backblaze B2 with rclone&lt;/a&gt;.&#10;Ideally that would be ZFS as well, and some hosting services accept &lt;code&gt;zfs send&lt;/code&gt;, but they cost far more than object storage.&#10;A friend with a ZFS box costs nothing.&lt;/p&gt;&#10;&lt;h2 id="rehearsing-the-permissions-in-a-vm"&gt;Rehearsing the permissions in a VM &lt;a class="anchor" href="#rehearsing-the-permissions-in-a-vm" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before writing any code, I had an agent build a throwaway NixOS VM test with a real pool, a delegated user, and a list of probes.&#10;Each probe tried one thing the friend should not be able to do and recorded what ZFS answered.&lt;/p&gt;&#10;&lt;p&gt;The first surprise came from the most obvious setup.&#10;With a plain &lt;code&gt;zfs allow -u joe create,receive,destroy,... tank/friends/joe&lt;/code&gt;, the delegated user could run &lt;code&gt;zfs destroy -r tank/friends/joe&lt;/code&gt; and delete the dataset I had created for him, quota included.&#10;Delegated rights apply to the dataset itself as well as everything below it, unless you say otherwise.&#10;The fix is to split them: on the root itself only &lt;code&gt;create,mount,receive&lt;/code&gt; with &lt;code&gt;zfs allow -l&lt;/code&gt;, and the full set only for descendants with &lt;code&gt;zfs allow -d&lt;/code&gt;.&#10;After that change, destroying, snapshotting, re-delegating, or changing the quota of the root all failed with &lt;code&gt;permission denied&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;A send stream can carry properties too, such as a hostile &lt;code&gt;mountpoint=/etc&lt;/code&gt;.&#10;ZFS applies them with the receiving user&amp;rsquo;s rights, so in the VM that property failed with &lt;code&gt;permission denied&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The probes also confirmed the gap: the delegated user saw my &lt;code&gt;tank/host&lt;/code&gt; dataset in &lt;code&gt;zfs list&lt;/code&gt;, and &lt;code&gt;zfs get used tank/host&lt;/code&gt; returned its size.&lt;/p&gt;&#10;&lt;h2 id="a-gate-that-speaks-syncoid"&gt;A gate that speaks syncoid &lt;a class="anchor" href="#a-gate-that-speaks-syncoid" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The shell part has a standard answer.&#10;An &lt;code&gt;authorized_keys&lt;/code&gt; line with &lt;code&gt;restrict,command=&amp;quot;...&amp;quot;&lt;/code&gt; runs one fixed program for every login, and passes whatever the client asked for in &lt;code&gt;SSH_ORIGINAL_COMMAND&lt;/code&gt;.&#10;&lt;code&gt;zfs-tenant gate&lt;/code&gt; is that program.&#10;It tokenizes the request, accepts only the handful of command shapes a backup needs, checks that every dataset name is inside the friend&amp;rsquo;s dataset, and runs &lt;code&gt;zfs&lt;/code&gt; with an argument list it builds itself.&#10;It never starts a shell, so there is nothing to inject into.&lt;/p&gt;&#10;&lt;p&gt;The hard part was knowing exactly which commands syncoid sends, so an agent read &lt;a href="https://github.com/jimsalterjrs/sanoid/blob/v2.3.0/syncoid" target="_blank" rel="noopener"&gt;syncoid&amp;rsquo;s Perl&lt;/a&gt; and logged every remote command it ran.&#10;Most are the &lt;code&gt;zfs&lt;/code&gt; commands you would expect.&#10;One surprise: before each receive, syncoid runs &lt;code&gt;ps -Ao args=&lt;/code&gt; on the receiving machine, to check that no other transfer is already writing to the same dataset.&#10;That command prints every running process with its full command line.&#10;On my NAS, it would show my friend everything I run, so the gate answers with an empty list.&lt;/p&gt;&#10;&lt;p&gt;Reading syncoid that closely also turned up a remote code execution (RCE) bug in version 2.3.0, outside zfs-tenant itself.&#10;In push mode, syncoid asks the receiving host for a ZFS resume token.&#10;It passed that answer unescaped into shell commands for estimating and resuming the send, which run on the source machine.&#10;A malicious or compromised backup host could therefore return shell syntax instead of a token and execute arbitrary commands on the sender with the privileges of the syncoid process.&lt;/p&gt;&#10;&lt;p&gt;I reproduced this in two isolated NixOS VMs using real ZFS and SSH: a harmless command embedded in the receiver&amp;rsquo;s answer created a marker file on the sender.&#10;I reported it to the maintainer and opened &lt;a href="https://github.com/jimsalterjrs/sanoid/pull/1114" target="_blank" rel="noopener"&gt;sanoid PR #1114&lt;/a&gt; with a small fix that shell-quotes the token at both uses.&#10;Even with that fix, the sending side should run syncoid as an unprivileged user that may only &lt;code&gt;send&lt;/code&gt; and &lt;code&gt;hold&lt;/code&gt;.&#10;nixpkgs&amp;rsquo; &lt;code&gt;services.syncoid&lt;/code&gt; already works that way, so my friend needs nothing from zfs-tenant at all.&lt;/p&gt;&#10;&lt;h2 id="zones-a-second-wall-in-the-kernel"&gt;Zones: a second wall in the kernel &lt;a class="anchor" href="#zones-a-second-wall-in-the-kernel" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;At that point the gate was the only thing hiding my datasets.&#10;Then my friend sent me a message: ZFS has a feature called zones, which restricts a dataset tree to a Linux user namespace.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-zone.8.html" target="_blank" rel="noopener"&gt;&lt;code&gt;zfs zone&lt;/code&gt;&lt;/a&gt; attaches a dataset to one user namespace, and inside that namespace the ZFS kernel module answers &lt;code&gt;dataset does not exist&lt;/code&gt; for everything that is not attached.&#10;Even if my gate had a bug that let arbitrary &lt;code&gt;zfs&lt;/code&gt; commands through, &lt;code&gt;tank/host&lt;/code&gt; would stay invisible.&#10;The zone does not replace the gate, though: it only filters ZFS, and it only applies once the gate has put the SSH session inside it.&#10;&lt;a href="#who-stops-what"&gt;Who stops what&lt;/a&gt; shows how the two divide the work.&lt;/p&gt;&#10;&lt;p&gt;A user namespace can give its processes different user IDs than they have outside.&#10;Containers usually use that to make the user root inside the namespace, while it stays an ordinary user outside.&#10;ZFS treats root inside a zone as the zone&amp;rsquo;s administrator, allowed to do anything to the attached datasets, whatever &lt;code&gt;zfs allow&lt;/code&gt; says.&#10;In the VM test, that let the friend destroy the dataset I had created for him.&#10;So zfs-tenant keeps him as his own ordinary user inside the namespace: he has no special rights there, &lt;code&gt;zfs allow&lt;/code&gt; decides what he may do, and the kernel still hides everything else.&lt;/p&gt;&#10;&lt;p&gt;Root inside a user namespace also turned up in a real OpenZFS bug in August: before 2.4.4, 2.3.9, and 2.2.11, several pool operations, such as destroying a pool, accepted it as host root (&lt;a href="https://github.com/openzfs/zfs/security/advisories/GHSA-mhf5-q8gw-qg9v" target="_blank" rel="noopener"&gt;CVE-2026-79619&lt;/a&gt;).&#10;Joe cannot reach that through the gate, but zfs-tenant needs unprivileged user namespaces, so a host running it should load a patched module: &lt;code&gt;cat /sys/module/zfs/version&lt;/code&gt; shows the loaded one, which only changes after a reboot.&lt;/p&gt;&#10;&lt;p&gt;A zone needs a running namespace to attach to, so zfs-tenant runs a small service per friend that holds one.&#10;The gate joins that namespace before it looks at the command, drops the capabilities that joining grants, and refuses to run if the service is down.&lt;/p&gt;&#10;&lt;h2 id="who-stops-what"&gt;Who stops what &lt;a class="anchor" href="#who-stops-what" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When Joe and I talked it through, I found it hard to say clearly which part protects against what, so I made this diagram.&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://www.nijho.lt/post/zfs-tenant/layers.svg" alt="Diagram of seven things Joe&amp;#39;s key tries and the layer that stops each one: the network, the SSH gate, the zone, and zfs allow with the quota. Only his raw encrypted backup reaches my pool, where it stays unreadable to me." loading="lazy" data-zoomable&gt;&lt;/figure&gt;&#10;&lt;p&gt;Each column answers one question: who can connect, what he can run, what ZFS shows him, and what he can change.&#10;For the network, our tailnet only lets his router reach port 22 on my NAS, and &lt;code&gt;from=&lt;/code&gt; in &lt;code&gt;authorized_keys&lt;/code&gt; only accepts his key from that address.&#10;Raw sends then keep whatever reaches my pool unreadable to me.&lt;/p&gt;&#10;&lt;p&gt;The gate and the zone overlap in one job: hiding my other datasets.&#10;The gate does it first, by refusing any name outside his root before &lt;code&gt;zfs&lt;/code&gt; runs.&#10;The zone repeats that check in the kernel, so hiding my datasets would take a bug in both.&#10;Everything else the gate does, the zone cannot:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Without the gate, his key would be a normal account on my NAS. The zone only filters ZFS; it would not stop him from reading &lt;code&gt;/etc&lt;/code&gt; or running programs.&lt;/li&gt;&#10;&lt;li&gt;An SSH login does not start inside a user namespace. The gate joins the zone before it runs anything, so without the gate the zone would never apply.&lt;/li&gt;&#10;&lt;li&gt;The zone leaves some things visible: &lt;code&gt;zpool status&lt;/code&gt; still shows my disks and how full the pool is, and the datasets above his root still show their sizes. User namespaces do not hide processes either, hence the empty answer to &lt;code&gt;ps&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;It deletes any unencrypted dataset he pushes by mistake, so I never keep his plaintext.&lt;/li&gt;&#10;&lt;li&gt;It logs every command, which is how I saw Joe try &lt;code&gt;bash&lt;/code&gt; below.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The &lt;a href="https://zfs-tenant.nijho.lt/security/" target="_blank" rel="noopener"&gt;security model&lt;/a&gt; in the documentation lists every promise with the mechanism behind it.&lt;/p&gt;&#10;&lt;h2 id="what-it-cannot-hide"&gt;What it cannot hide &lt;a class="anchor" href="#what-it-cannot-hide" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;ZFS encryption protects file contents, not structure.&#10;In the words of &lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-load-key.8.html" target="_blank" rel="noopener"&gt;&lt;code&gt;zfs-load-key(8)&lt;/code&gt;&lt;/a&gt;, ZFS &amp;ldquo;will not encrypt metadata related to the pool structure, including dataset and snapshot names, dataset hierarchy, properties, file size, file holes, and deduplication tables.&amp;rdquo;&#10;So I can see the names, sizes, and snapshot times of whatever my friend sends me.&#10;Boring dataset names help, and sending without &lt;code&gt;-p&lt;/code&gt; keeps properties out of the stream.&lt;/p&gt;&#10;&lt;p&gt;I can also always delete his copy, because I am root on my own machine.&#10;What I can never do is read it.&#10;My &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/storage.nix#L71-L81" target="_blank" rel="noopener"&gt;monthly scrubs&lt;/a&gt; still verify his data without his key, because ZFS checksums the encrypted blocks.&lt;/p&gt;&#10;&lt;p&gt;What we protect against is a dead machine: a failed pool, a fire, a flood.&#10;His key may destroy anything below his root, which is what lets syncoid mirror his snapshot retention, so someone who steals that key can also delete his backups on my NAS.&#10;Covering that would take holds that I place as root and release on a schedule.&#10;We left them out on purpose, although I am &lt;a href="#ai-scope-creep-or-useful"&gt;reconsidering&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="setting-it-up"&gt;Setting it up &lt;a class="anchor" href="#setting-it-up" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;On a NixOS host, giving a friend space takes this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zfs-tenant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tenants&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;joe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tank/friends/joe&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;quota&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2T&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;authorizedKeys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... joe-nas&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;allowedFrom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;100.64.0.12&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;# his tailnet address&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That creates the user, pins the key to the gate, applies the dataset, properties, and delegation on every boot, and runs the zone service.&#10;My own setup is in &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/friend-backups.nix#L43-L50" target="_blank" rel="noopener"&gt;&lt;code&gt;friend-backups.nix&lt;/code&gt;&lt;/a&gt;, with his real key and address.&#10;If your sanoid snapshots the whole pool, &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/friend-backups.nix#L52-L59" target="_blank" rel="noopener"&gt;exclude the tenant tree&lt;/a&gt;: your sanoid must neither snapshot nor prune it, or his next incremental push finds snapshots on the target that he never sent.&#10;Once my friend is on NixOS, he can push with nixpkgs&amp;rsquo; own module:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;programs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;knownHosts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bas-nas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;syncoid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sshKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/var/lib/syncoid/id_ed25519&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;localSourceAllow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;send&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hold&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;commonArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;--no-sync-snap&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;--compress=none&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;--delete-target-snapshots&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;--sshoption=StrictHostKeyChecking=yes&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;tank/offsite&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;zfs-tenant-joe@bas-nas:tank/friends/joe/offsite&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;recursive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sendOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My NAS will push to his with the &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/friend-backups.nix#L66-L91" target="_blank" rel="noopener"&gt;same configuration&lt;/a&gt; once he hosts a root for me.&lt;/p&gt;&#10;&lt;p&gt;For his TrueNAS box, every &lt;a href="https://github.com/basnijholt/zfs-tenant/releases" target="_blank" rel="noopener"&gt;release&lt;/a&gt; has a single &lt;code&gt;zfs-tenant.pyz&lt;/code&gt; that runs with the Python TrueNAS already ships, and the &lt;a href="https://zfs-tenant.nijho.lt/getting-started/" target="_blank" rel="noopener"&gt;getting started guide&lt;/a&gt; covers both setups.&#10;Hosting on TrueNAS has only run in VMs so far (&lt;a href="https://github.com/basnijholt/zfs-tenant/issues/4" target="_blank" rel="noopener"&gt;issue #4&lt;/a&gt;).&#10;Sending from TrueNAS is where its built-in tools stop working: replication tasks wrap every remote command in &lt;code&gt;sh -c&lt;/code&gt;, which the gate refuses, so he pushes with &lt;code&gt;zfs send&lt;/code&gt; or syncoid instead.&lt;/p&gt;&#10;&lt;h2 id="knowing-when-it-stops-working"&gt;Knowing when it stops working &lt;a class="anchor" href="#knowing-when-it-stops-working" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The &lt;a href="https://www.nijho.lt/post/btrfs-to-zfs/"&gt;last btrfs machine&lt;/a&gt; taught me a fifth question for any backup: how do I find out when it stops working?&#10;A failed push shows up in &lt;code&gt;systemctl status&lt;/code&gt; on the sending side, which nobody reads.&#10;The check belongs on the receiving side, and it has to look at every pushed dataset separately, because one healthy dataset can hide another that stopped replicating.&#10;That is how I watch my own machines: the NAS &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/replication.nix#L332-L352" target="_blank" rel="noopener"&gt;checks the newest snapshot of every dataset it replicates&lt;/a&gt; every hour and alerts my phone when one is too old.&#10;zfs-tenant does not alert yet; &lt;a href="https://github.com/basnijholt/zfs-tenant/issues/3" target="_blank" rel="noopener"&gt;issue #3&lt;/a&gt; tracks it.&lt;/p&gt;&#10;&lt;h2 id="restoring"&gt;Restoring &lt;a class="anchor" href="#restoring" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The gate also allows &lt;code&gt;zfs send&lt;/code&gt; of his own snapshots, so getting data back is one pipe, run at his house:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh zfs-tenant-joe@bas-nas zfs send -w tank/friends/joe/offsite/photos@autosnap_2026-09-25_00:00:01_daily &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;|&lt;/span&gt; zfs receive -u tank/photos&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zfs load-key tank/photos&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;zfs mount tank/photos&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My photos are a child dataset that inherits its key from an encryption root, and I only push the child.&#10;If my NAS dies and I pull the child back, can I still unlock it without the root?&#10;A throwaway VM says yes: a raw send carries the dataset&amp;rsquo;s own wrapped key, so the child arrives as its own encryption root and opens with the same passphrase.&#10;The one catch: after a &lt;code&gt;zfs change-key&lt;/code&gt;, the next push carries the new wrapping, so the passphrase that matters is the one at the time of the last push, and it has to live somewhere other than the NAS.&#10;Mine does: &lt;a href="https://github.com/basnijholt/zfs-unlock" target="_blank" rel="noopener"&gt;zfs-unlock&lt;/a&gt; keeps the passphrases on a separate device and &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/zfs-unlock.nix" target="_blank" rel="noopener"&gt;unlocks the NAS&lt;/a&gt; after every boot.&lt;/p&gt;&#10;&lt;h2 id="the-first-real-push"&gt;The first real push &lt;a class="anchor" href="#the-first-real-push" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Once &lt;a href="https://github.com/basnijholt/dotfiles/blob/6526b50e9bae1449a915239491099ef71528f10c/configs/nixos/hosts/nas/friend-backups.nix" target="_blank" rel="noopener"&gt;my side was deployed&lt;/a&gt;, Joe tried it from his TrueNAS box.&#10;His first move was to see what else his key could do.&#10;The gate logged every attempt on my NAS:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root=tank/friends/joe denied interactive session&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root=tank/friends/joe denied &amp;#39;ls&amp;#39;: only zfs commands and syncoid&amp;#39;s probes are allowed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root=tank/friends/joe denied &amp;#39;/usr/bin/bash&amp;#39;: only zfs commands and syncoid&amp;#39;s probes are allowed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root=tank/friends/joe denied &amp;#39;/bin/bash&amp;#39;: only zfs commands and syncoid&amp;#39;s probes are allowed&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;root=tank/friends/joe allowed &amp;#39;zfs receive -s -u tank/friends/joe/zt-test&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The last line is a small encrypted test dataset, sent with &lt;code&gt;zfs send -w&lt;/code&gt; piped into &lt;code&gt;ssh&lt;/code&gt;, without syncoid.&#10;It arrived with &lt;code&gt;keystatus&lt;/code&gt; set to &lt;code&gt;unavailable&lt;/code&gt;.&#10;Then I tried to read it as root on my own machine.&#10;&lt;code&gt;zfs mount&lt;/code&gt; answered &lt;code&gt;cannot mount 'tank/friends/joe/zt-test': dataset is exported to a local zone&lt;/code&gt;, and &lt;code&gt;zfs load-key&lt;/code&gt; with a guessed passphrase gave &lt;code&gt;Key load error: Incorrect key provided&lt;/code&gt;.&#10;The only readable text in the raw stream was ZFS property names and the snapshot name, which is the metadata ZFS leaves unencrypted, as described above.&lt;/p&gt;&#10;&lt;h2 id="tested-like-everything-else"&gt;Tested like everything else &lt;a class="anchor" href="#tested-like-everything-else" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Every security claim in this post is also checked by a VM test: two NixOS machines back each other up, the way my friend and I do, with real sanoid and syncoid 2.3.0, through real sshd and the gate, into real OpenZFS 2.4.4.&#10;It also checks what happens over time, such as snapshot retention and both machines rebooting, and it runs on every push in GitHub Actions.&lt;/p&gt;&#10;&lt;p&gt;The route to the &lt;a href="https://github.com/jimsalterjrs/sanoid/pull/1114" target="_blank" rel="noopener"&gt;remote code execution bug in syncoid&lt;/a&gt; was slightly absurd.&#10;I built the initial version of zfs-tenant through coding agents, literally from the bathtub, and then asked several frontier models to review the security boundary independently.&#10;One of them followed receiver-controlled data beyond my new code and into syncoid.&#10;A model&amp;rsquo;s suspicion is not evidence, so I turned it into the two-VM reproduction above: it failed on unpatched syncoid 2.3.0 and passed after the seven-line fix.&#10;The most consequential result of reviewing my small new project was a command-injection bug in the mature tool next to it.&lt;/p&gt;&#10;&lt;p&gt;The whole thing is about 930 lines of Python, not counting comments and docstrings, with no dependencies.&#10;The kernel does the actual enforcing: delegation, the quota, and the zone.&#10;The gate only removes the shell, and it is small enough to read in one sitting.&lt;/p&gt;&#10;&lt;h2 id="ai-scope-creep-or-useful"&gt;AI scope creep or useful? &lt;a class="anchor" href="#ai-scope-creep-or-useful" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The first version had &lt;em&gt;grace holds&lt;/em&gt;: every day, the host placed a hold on the newest snapshot of each of my friend&amp;rsquo;s datasets and released it 14 days later.&#10;ZFS will not destroy a held snapshot until the hold is released, and only the host can release it.&#10;The agent built it without me asking for it, so I removed it right away as scope creep.&lt;/p&gt;&#10;&lt;p&gt;Then every model I asked to review the project suggested them again, each in its own context window: Opus 5.5, GPT-6 Astra, and even the one that reviewed a draft of this post.&#10;Joe and I had set out to protect against a machine dying (a fire or a flood), not against one of our own machines being hacked.&#10;But I am tempted to add the holds back.&#10;They cover the case where malware or an attacker gets onto my machine and uses my key to delete my backups on his NAS, or to replace them with garbage.&#10;Adding the feature without asking was still wrong; the feature itself might not be.&lt;/p&gt;&#10;&lt;h2 id="references"&gt;References &lt;a class="anchor" href="#references" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/zfs-tenant" target="_blank" rel="noopener"&gt;zfs-tenant on GitHub&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://zfs-tenant.nijho.lt" target="_blank" rel="noopener"&gt;zfs-tenant documentation&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/truenas-remote-backups/"&gt;Friend-to-friend TrueNAS backups without the cloud&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/truenas-to-nixos/"&gt;Migrating from TrueNAS to NixOS&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/btrfs-to-zfs/"&gt;The last btrfs machine: migrating my PC to ZFS&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-allow.8.html" target="_blank" rel="noopener"&gt;&lt;code&gt;zfs-allow(8)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://openzfs.github.io/openzfs-docs/man/master/8/zfs-zone.8.html" target="_blank" rel="noopener"&gt;&lt;code&gt;zfs-zone(8)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/jimsalterjrs/sanoid" target="_blank" rel="noopener"&gt;Sanoid and syncoid&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>MindRoom: AI agents that live in Matrix and work everywhere 🧠</title><link>https://www.nijho.lt/post/mindroom/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/mindroom/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-the-problem-your-ai-is-trapped-in-apps"&gt;1. The problem: your AI is trapped in apps&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-what-is-mindroom"&gt;2. What is MindRoom?&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-how-it-actually-works"&gt;3. How it actually works&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-building-on-matrix-the-good-and-the-tricky"&gt;4. Building on Matrix: the good and the tricky&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-memory-that-follows-you"&gt;5. Memory that follows you&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-100-built-in-tool-integrations"&gt;6. 100+ built-in tool integrations&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-teams-agents-that-collaborate"&gt;7. Teams: agents that collaborate&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-hot-reload-change-config-and-plugins-without-downtime"&gt;8. Hot-reload: change config and plugins without downtime&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-voice-scheduling-and-other-features"&gt;9. Voice, scheduling, and other features&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#10-the-obsession-the-saas-dream-and-the-burnout"&gt;10. The obsession, the SaaS dream, and the burnout&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#11-openclaw-and-why-it-matters"&gt;11. OpenClaw and why it matters&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#12-whats-next"&gt;12. What&amp;rsquo;s next&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#links-and-resources"&gt;Links and resources&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-the-problem-your-ai-is-trapped-in-apps"&gt;1. The problem: your AI is trapped in apps &lt;a class="anchor" href="#1-the-problem-your-ai-is-trapped-in-apps" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Programming is my biggest passion in life.&#10;I&amp;rsquo;ve been actively involved in open source for over ten years now, maintaining &lt;a href="https://github.com/basnijholt" target="_blank" rel="noopener"&gt;40+ installable packages&lt;/a&gt; across Python, JavaScript, and Rust.&#10;Some of these have over 100,000 users; others have exactly one—me.&#10;I mention this because it matters for what comes next: when AI coding tools arrived, I didn&amp;rsquo;t start from zero.&#10;As I wrote about in my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic coding post&lt;/a&gt;, AI has completely transformed how I work—when I&amp;rsquo;m AFK, I regularly build full features or entire projects by &lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;dictating to my phone&lt;/a&gt; and routing it through my own &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;transcription software&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Through all of this, I became deeply convinced that AI agents are the future.&#10;Not just a useful tool for programmers—a fundamental shift in how &lt;em&gt;everyone&lt;/em&gt; will interact with computers.&lt;/p&gt;&#10;&lt;p&gt;But if that&amp;rsquo;s true, then the way agents work today is broken.&lt;/p&gt;&#10;&lt;p&gt;We&amp;rsquo;ve built artificial barriers between artificial intelligences.&#10;ChatGPT knows you in one tab, Claude knows you in another, your Slack bot knows you in a third.&#10;Your email AI can&amp;rsquo;t tell your calendar AI about that meeting invitation.&#10;Your coding assistant has no idea about the project specs sitting in your document AI.&#10;Switch platforms, and your AI starts from scratch.&lt;/p&gt;&#10;&lt;p&gt;Most agent frameworks require you to &lt;em&gt;program&lt;/em&gt; them.&#10;That works for developers like me, but if agents are truly the future, they need to work for everyone.&#10;Non-programmers don&amp;rsquo;t want to write code—they just want a chat.&lt;/p&gt;&#10;&lt;p&gt;And then there&amp;rsquo;s privacy.&#10;I&amp;rsquo;m fine sharing my email with Gemini—Google already owns my Gmail.&#10;But do I want to send my financial data, my health records, or my personal notes to a cloud provider?&#10;Not really.&#10;I&amp;rsquo;d rather use a local model for sensitive tasks and pick the best, cheapest Chinese model for general-purpose deep research.&lt;/p&gt;&#10;&lt;p&gt;So I built &lt;strong&gt;MindRoom&lt;/strong&gt; on a chat protocol—if people just want a chat, give them a chat.&#10;Agents live in the same rooms as you, collaborate in threads, and follow you across every platform.&#10;The first commit landed on July 29, 2025 (four months before OpenClaw&amp;rsquo;s first commit 😅), so by the time I&amp;rsquo;m publishing this MindRoom has been with me for nearly eleven months: first as an obsessive side project, then, since March, with much more sustained focus.&#10;At first I got so obsessed with it that I eventually burned out and had to step away completely.&lt;/p&gt;&#10;&lt;p&gt;Recently, seeing &lt;a href="https://openclaw.ai" target="_blank" rel="noopener"&gt;OpenClaw&lt;/a&gt; gain massive traction solving a similar problem reminded me that what I&amp;rsquo;d been building wasn&amp;rsquo;t just a niche obsession.&#10;So here I am, dusting off MindRoom and writing about what it is, how it works, and why I think the approach still matters.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; MindRoom is an open-source system that creates AI agents living inside the &lt;a href="https://matrix.org/" target="_blank" rel="noopener"&gt;Matrix protocol&lt;/a&gt;.&#10;Because Matrix has bridges to Slack, Telegram, Discord, WhatsApp, and everything else, your agents can follow you across every platform—with persistent memory, 100+ built-in tool integrations, and multi-agent collaboration.&#10;Think of it as &amp;ldquo;your AI assistant, but it lives in Matrix and works everywhere.&amp;rdquo;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;figure id="figure-live-chatmindroomchat-personal-thread-with-the-prompt-explain-how-mindroom-and-what-you-can-do-mind-answers-in-the-real-matrix-room-using-tool-calls-and-the-codex-backed-local-mindroom-runtime"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_5c1001a61c1ce0a1.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_cba228417596d894.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_5c1001a61c1ce0a1.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_46631ca52a25e910.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="489" alt="Live MindRoom Personal thread showing the prompt Explain how MindRoom and what you can do, followed by Mind explaining Matrix agents, tools, memory, and skills." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Live chat.mindroom.chat Personal thread with the prompt: “Explain how MindRoom and what you can do.” Mind answers in the real Matrix room, using tool calls and the Codex-backed local MindRoom runtime.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="2-what-is-mindroom"&gt;2. What is MindRoom? &lt;a class="anchor" href="#2-what-is-mindroom" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;MindRoom is an open-source system that creates AI agents living inside the &lt;a href="https://matrix.org/" target="_blank" rel="noopener"&gt;Matrix protocol&lt;/a&gt;.&#10;If you&amp;rsquo;re not familiar with Matrix—it&amp;rsquo;s a federated, end-to-end encrypted communication standard.&#10;The same protocol used by the French government for 5.5 million civil servants, by German healthcare for 150K+ organizations, and by the Element app that millions of people use daily.&lt;/p&gt;&#10;&lt;p&gt;The key insight: Matrix has &lt;a href="https://matrix.org/ecosystem/bridges/" target="_blank" rel="noopener"&gt;bridges&lt;/a&gt; to many major platforms—Slack, Telegram, Discord, WhatsApp, IRC, email, even SMS.&lt;/p&gt;&#10;&lt;p&gt;So if your AI agent lives in Matrix, it can reach you on any platform.&#10;One agent, every platform, continuous memory.&lt;/p&gt;&#10;&lt;p&gt;One practical caveat: Matrix bridges vary in maturity. Some, like the &lt;a href="https://github.com/mautrix/telegram" target="_blank" rel="noopener"&gt;Telegram bridge&lt;/a&gt;, work very well, while others can be finicky. Your mileage may vary depending on which platforms you need.&lt;/p&gt;&#10;&lt;p&gt;Here&amp;rsquo;s what a typical setup looks like in &lt;code&gt;config.yaml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;CodeAgent&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Generate code, manage files, execute shell commands&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;opus-4.8&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;file, shell, github]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;Always read files before modifying them.&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;lobby, dev]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;research&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ResearchAgent&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Search the web, summarize papers, find information&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;gpt-5.5&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;tavily, arxiv, wikipedia]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;lobby, research]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;super_team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Super Team&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;code, research]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;collaborate&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Define your agents, give them tools and rooms, and they show up in Matrix as real users—with avatars, typing indicators, and online status.&lt;/p&gt;&#10;&lt;figure id="figure-mindroom-agents-are-real-matrix-users-with-their-own-accounts-generated-avatars-display-names-room-memberships-and-online-presence"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-members_hu_5090cd21f23bad91.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-members_hu_fbc884b2330f0fe3.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-members_hu_5090cd21f23bad91.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-members_hu_e6a566cfcdf2d915.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="489" alt="Live MindRoom room showing RouterAgent, Mind, and Bas as Matrix users with generated avatars." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;MindRoom agents are real Matrix users with their own accounts, generated avatars, display names, room memberships, and online presence.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="3-how-it-actually-works"&gt;3. How it actually works &lt;a class="anchor" href="#3-how-it-actually-works" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;At the core sits what I call the &lt;strong&gt;MultiAgentOrchestrator&lt;/strong&gt;—a class in &lt;code&gt;bot.py&lt;/code&gt; that boots every configured entity (router, agents, teams), provisions Matrix user accounts for each one via &lt;a href="https://github.com/matrix-nio/matrix-nio" target="_blank" rel="noopener"&gt;matrix-nio&lt;/a&gt;, and keeps sync loops alive.&#10;The agents themselves are powered by the &lt;a href="https://github.com/agno-agi/agno" target="_blank" rel="noopener"&gt;Agno&lt;/a&gt; framework, which provides a unified interface across AI model providers.&lt;/p&gt;&#10;&lt;p&gt;When someone sends a message in a Matrix room:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Explicit mentions&lt;/strong&gt; go directly to the named agent (&lt;code&gt;@mindroom_code help me debug this&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Thread continuations&lt;/strong&gt; keep the same agent responding (no weird context switches mid-conversation)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;New conversations&lt;/strong&gt; hit the &lt;strong&gt;router&lt;/strong&gt;—an AI that analyzes the message and picks the best agent based on capabilities&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;All conversations happen in threads, which keeps rooms organized.&#10;Agents stream their responses in real-time, editing a single message as they think rather than spamming new ones.&#10;You see tool calls happening live:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;🔧 Tool Call: search_web(query=&amp;#34;matrix protocol bridges&amp;#34;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[waiting for result...]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ search_web result:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[results here]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The recording below starts in the main Personal room, sends the prompt &lt;code&gt;Explain how MindRoom and what you can do.&lt;/code&gt;, and then follows MindRoom as it opens the new thread and streams the reply to completion. It is sped up 2.5x to keep the flow skimmable.&lt;/p&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/reduced-motion.3dfea0061c17c60a9a200c61ef1e3b3e98d5bdc4eaa4e0a64b3ed2a4d13c72fd.js" integrity="sha256-Pf6gBhwXxgqaIAxh7x47PpjVvcTqpOCmSz7SpNE8cv0=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;video controls autoplay muted playsinline loop preload="metadata"&gt;&#10;&lt;source src="https://www.nijho.lt/post/mindroom/mindroom-agent-response.mp4" type="video/mp4"&gt;&#10;&lt;/video&gt;&#10;&#10;&lt;h2 id="4-building-on-matrix-the-good-and-the-tricky"&gt;4. Building on Matrix: the good and the tricky &lt;a class="anchor" href="#4-building-on-matrix-the-good-and-the-tricky" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the best things about building on Matrix is what you get for free.&#10;End-to-end encryption, a deeply interoperable chat protocol with bridges to many platforms, a choice of clients, federation between organizations—all of that comes with the protocol.&#10;You don&amp;rsquo;t have to build any of it yourself.&lt;/p&gt;&#10;&lt;p&gt;But because Matrix has such a tight specification, it also brings challenges.&lt;/p&gt;&#10;&lt;p&gt;The protocol doesn&amp;rsquo;t support streaming.&#10;AI agents that think for 30 seconds before dumping a wall of text make for a terrible chat experience, so I hacked streaming in by rapidly editing the same message as new tokens arrive.&#10;An &lt;code&gt;⋯&lt;/code&gt; marker shows while the agent is still thinking—a small touch, but it makes the experience feel responsive and alive.&lt;/p&gt;&#10;&lt;p&gt;There&amp;rsquo;s also a size limit on message content.&#10;That&amp;rsquo;s fine for human chat, but AI responses can get long—especially when tool calls and their results are included.&#10;I worked around this by using Matrix&amp;rsquo;s attachment feature: when a response exceeds the limit, the content continues in an attachment that gets updated as the message keeps streaming in.&#10;This required forking the &lt;a href="https://cinny.in/" target="_blank" rel="noopener"&gt;Cinny&lt;/a&gt; chat client so that attachments display inline rather than as downloadable files, making the whole thing seamless.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;These workarounds are the kind of thing you don&amp;rsquo;t anticipate when you pick a protocol.&#10;Matrix gives you an incredible foundation, but making it work well for AI required bending it in ways it wasn&amp;rsquo;t designed for.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="5-memory-that-follows-you"&gt;5. Memory that follows you &lt;a class="anchor" href="#5-memory-that-follows-you" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;MindRoom currently supports two memory implementations.&lt;/p&gt;&#10;&lt;p&gt;The first is the more traditional semantic memory system built on &lt;a href="https://mem0.ai/" target="_blank" rel="noopener"&gt;Mem0&lt;/a&gt;&amp;rsquo;s &lt;code&gt;AsyncMemory&lt;/code&gt;, with configurable embedding providers (&lt;a href="https://platform.openai.com/docs/guides/embeddings" target="_blank" rel="noopener"&gt;OpenAI&lt;/a&gt;, &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt;, or HuggingFace) and vector storage via &lt;a href="https://www.trychroma.com/" target="_blank" rel="noopener"&gt;ChromaDB&lt;/a&gt;.&#10;This gives agents searchable memory for preferences, project context, decisions, and recurring facts.&lt;/p&gt;&#10;&lt;p&gt;The second is a file-based memory system inspired by the simplicity of OpenClaw-style agent files.&#10;Instead of burying identity in a database, durable context can live in plain Markdown files that travel with the agent.&#10;That portability matters: I can move the same agent identity from OpenClaw to Hermes to MindRoom without starting from scratch.&lt;/p&gt;&#10;&lt;p&gt;MindRoom also has a memory flush step that periodically extracts durable facts from conversations and writes them back into memory.&#10;So the chat transcript remains chat, while stable preferences, decisions, and identity details become reusable context for future turns.&lt;/p&gt;&#10;&lt;figure id="figure-a-live-personal-memory-demo-mindroom-stores-a-harmless-preference-in-one-turn-and-retrieves-it-later-from-the-same-matrix-conversation"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-memory_hu_5090cd21f23bad91.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-memory_hu_fbc884b2330f0fe3.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-memory_hu_5090cd21f23bad91.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-memory_hu_e6a566cfcdf2d915.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="489" alt="Live MindRoom memory thread showing the generated agent avatars and a public demo where the agent remembers teal as the blog demo accent color." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;A live Personal memory demo: MindRoom stores a harmless preference in one turn and retrieves it later from the same Matrix conversation.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="6-100-built-in-tool-integrations"&gt;6. 100+ built-in tool integrations &lt;a class="anchor" href="#6-100-built-in-tool-integrations" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Agents can use over 100 built-in integrations:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Category&lt;/th&gt;&#10;&lt;th&gt;Examples&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Communication&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Gmail, Slack, Telegram, Discord&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Development&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;GitHub, Shell, Python, Docker&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Search&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Tavily, Wikipedia, Arxiv, DuckDuckGo&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Productivity&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Google Calendar, Jira, Linear, Todoist&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Smart Home&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Home Assistant&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;AI/ML&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;DALL-E, ElevenLabs, Replicate&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Pandas, SQL, DuckDB, Yahoo Finance&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Web&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Firecrawl, Crawl4ai, Browser automation&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Tools are lazy-loaded and credential-managed, so an agent only loads what it needs.&lt;/p&gt;&#10;&lt;h2 id="7-teams-agents-that-collaborate"&gt;7. Teams: agents that collaborate &lt;a class="anchor" href="#7-teams-agents-that-collaborate" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Single agents are useful, but sometimes you need a team.&#10;MindRoom supports two collaboration modes:&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Coordinate mode&lt;/strong&gt;: A lead agent orchestrates others.&#10;You ask a question, the lead delegates subtasks, collects results, and synthesizes a unified response.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Collaborate mode&lt;/strong&gt;: All agents work on the same task in parallel, each providing their independent analysis.&#10;The system then merges their responses with a consensus summary.&lt;/p&gt;&#10;&lt;p&gt;In practice, you might have a research team where one agent searches academic papers, another checks industry news, and a third validates claims—all triggered by a single message.&#10;The live room view below shows a real team response in the same Matrix thread surface normal agents use: threads, routed agents, and room history are all first-class chat objects rather than a separate dashboard.&lt;/p&gt;&#10;&lt;figure id="figure-live-lobby-thread-in-chatmindroomchat-showing-a-real-team-response-from-homeassistant-and-agentbuilder-team-collaboration-uses-the-same-matrix-native-interface-as-regular-agent-chats-with-routed-agents-and-room-membership-visible-beside-the-conversation"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-team_hu_afbbb6ff5ac54e3f.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-team_hu_bf973e9c13f8cf6.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-team_hu_afbbb6ff5ac54e3f.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-team_hu_ee227208566f8d1.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="533" alt="Live MindRoom Lobby thread showing a Team Response from HomeAssistant and AgentBuilder, with the member list showing Super Team and other agents." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Live Lobby thread in chat.mindroom.chat showing a real Team Response from HomeAssistant and AgentBuilder. Team collaboration uses the same Matrix-native interface as regular agent chats, with routed agents and room membership visible beside the conversation.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="8-hot-reload-change-config-and-plugins-without-downtime"&gt;8. Hot-reload: change config and plugins without downtime &lt;a class="anchor" href="#8-hot-reload-change-config-and-plugins-without-downtime" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;config.yaml&lt;/code&gt; is watched at runtime.&#10;When you edit it—add an agent, change a model, update instructions—MindRoom diffs the old and new config, gracefully restarts only the affected agents, and has them rejoin their rooms.&#10;No downtime, no full runtime restart required.&lt;/p&gt;&#10;&lt;p&gt;The same development loop applies to plugins.&#10;MindRoom has a rich Python hook system, and plugins can be live-developed while the system is running.&#10;Change a plugin file, save it, and MindRoom automatically reloads the plugin without restarting the Python runtime.&lt;/p&gt;&#10;&lt;p&gt;This sounds minor, but when you&amp;rsquo;re iterating on agent behavior or plugin hooks, being able to tweak code and config and see results in seconds significantly improves the development loop.&lt;/p&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/reduced-motion.3dfea0061c17c60a9a200c61ef1e3b3e98d5bdc4eaa4e0a64b3ed2a4d13c72fd.js" integrity="sha256-Pf6gBhwXxgqaIAxh7x47PpjVvcTqpOCmSz7SpNE8cv0=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;video controls autoplay muted playsinline loop preload="metadata"&gt;&#10;&lt;source src="https://www.nijho.lt/post/mindroom/mindroom-hot-reload.mp4" type="video/mp4"&gt;&#10;&lt;/video&gt;&#10;&#10;&lt;h2 id="9-voice-scheduling-and-other-features"&gt;9. Voice, scheduling, and other features &lt;a class="anchor" href="#9-voice-scheduling-and-other-features" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Some features I built purely because I wanted them for myself (a recurring theme in my projects, as anyone who&amp;rsquo;s read my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt; can attest).&#10;I even built &lt;a href="https://github.com/basnijholt/matty" target="_blank" rel="noopener"&gt;Matty&lt;/a&gt;, a Matrix CLI client, from bed at midnight because I needed a way to interact with my agents from the terminal:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Voice messages&lt;/strong&gt;: Audio messages in Matrix are auto-transcribed via Whisper and treated as regular text input.&#10;You can talk to your agents.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Scheduled tasks&lt;/strong&gt;: Natural language scheduling (&lt;code&gt;!schedule &amp;quot;check my email every morning at 9 AM&amp;quot;&lt;/code&gt;) backed by cron jobs.&#10;Agents can run tasks in the background and escalate to you when needed.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;DM support&lt;/strong&gt;: Agents respond naturally in 1:1 conversations without needing mentions.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cross-organization federation&lt;/strong&gt;: Because Matrix is federated, two companies&amp;rsquo; AI agents can collaborate in a shared room—something that&amp;rsquo;s hard to do on proprietary platforms.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;The voice feature pairs nicely with my &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; tool—local Whisper transcription on my RTX 3090 means I can talk to my Matrix agents without any cloud dependency.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="10-the-obsession-the-saas-dream-and-the-burnout"&gt;10. The obsession, the SaaS dream, and the burnout &lt;a class="anchor" href="#10-the-obsession-the-saas-dream-and-the-burnout" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I need to be honest about what actually happened with MindRoom, because &amp;ldquo;life got in the way&amp;rdquo; is a sanitized version of the story.&lt;/p&gt;&#10;&lt;p&gt;What really happened is that I got completely, utterly obsessed.&#10;Every single second I wasn&amp;rsquo;t working at my day job or sleeping, I was working on MindRoom.&#10;Many hundreds of hours went into it.&#10;I seriously considered quitting my job and starting an AI startup around it.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;ve read Armin Ronacher&amp;rsquo;s post &lt;a href="https://lucumr.pocoo.org/2026/1/18/agent-psychosis/" target="_blank" rel="noopener"&gt;Agent Psychosis: Are We Going Insane?&lt;/a&gt;, you&amp;rsquo;ll know exactly what I&amp;rsquo;m talking about.&#10;The dopamine hit from building with AI agents is incredibly real.&#10;As Armin writes: &amp;ldquo;You feel productive, you feel like everything is amazing, and if you hang out just with people that are into that stuff too, without any checks, you go deeper and deeper into the belief that this all makes perfect sense.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;That was me.&#10;I was building and building, shipping feature after feature, and it felt incredible.&#10;The codebase grew to over 1,000 commits.&#10;I built the core system, then a React dashboard, then I started on a full SaaS platform—Kubernetes deployments on Hetzner Cloud, a FastAPI backend, a Next.js 15 frontend, Stripe integration, Supabase auth, Helm charts for multi-tenant isolation.&lt;/p&gt;&#10;&lt;p&gt;And that&amp;rsquo;s where the enthusiasm started to erode.&#10;Not because the core idea was bad, but because the work shifted from building interesting things (agent orchestration, memory systems, routing intelligence) to grinding through SaaS boilerplate: GDPR compliance, payment processing, automated Kubernetes deployments, terms of service, cookie banners.&#10;The dopamine loop that kept me going at 2 AM broke when the work stopped being creative and started being compliance paperwork.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Looking back, I think this is a pattern worth recognizing.&#10;Agentic coding makes it so easy to &lt;em&gt;start&lt;/em&gt; things that you can build yourself into a scope that would normally require a team.&#10;The gap between &amp;ldquo;I can build this&amp;rdquo; and &amp;ldquo;I can maintain and ship this as a product&amp;rdquo; is larger than the tools make it feel.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;The hosted control plane lives at &lt;a href="https://app.mindroom.chat" target="_blank" rel="noopener"&gt;app.mindroom.chat&lt;/a&gt;.&#10;I am deliberately not turning this section into a dashboard tour; the interesting part, and the part I still care about most, is the Matrix-native runtime underneath it.&lt;/p&gt;&#10;&lt;h2 id="11-openclaw-and-why-it-matters"&gt;11. OpenClaw and why it matters &lt;a class="anchor" href="#11-openclaw-and-why-it-matters" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://openclaw.ai" target="_blank" rel="noopener"&gt;OpenClaw&lt;/a&gt; takes a different approach to the same core problem.&#10;Where MindRoom builds on Matrix federation as the backbone, OpenClaw runs a local Gateway on your machine and connects to messaging platforms directly (WhatsApp via Baileys, Telegram via grammY, Discord via discord.js, and so on).&lt;/p&gt;&#10;&lt;p&gt;The similarities are striking:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Both give you a single AI assistant across all your messaging platforms&lt;/li&gt;&#10;&lt;li&gt;Both have persistent memory&lt;/li&gt;&#10;&lt;li&gt;Both support multiple tools and integrations&lt;/li&gt;&#10;&lt;li&gt;Both are open-source and privacy-focused&lt;/li&gt;&#10;&lt;li&gt;Both let you choose your own AI model&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The differences are interesting too.&#10;OpenClaw is local-first and TypeScript-based with 380K+ stars and a massive community.&#10;MindRoom is Python-based, federation-first, and&amp;hellip; well, let&amp;rsquo;s just say my GitHub star count is a few orders of magnitude lower.&lt;/p&gt;&#10;&lt;p&gt;But seeing OpenClaw validate the core idea—that people want AI assistants that aren&amp;rsquo;t trapped in apps—reminded me that what I&amp;rsquo;d been building matters.&#10;The federation angle is something OpenClaw doesn&amp;rsquo;t have: the ability for agents from different organizations to collaborate natively, with end-to-end encryption (Olm/Megolm), on a protocol that governments already trust and deploy at scale.&lt;/p&gt;&#10;&lt;figure id="figure-mindroom-uses-matrix-as-the-federated-backbone-openclaw-takes-a-local-gateway-direct-connector-approach"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_aaf71cef609b8340.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_2cca5883511aed87.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_aaf71cef609b8340.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_afb78d90679bf4a4.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="507" alt="Architecture diagram comparing MindRoom and OpenClaw. MindRoom flows from user to Matrix room to federated homeserver to bridges to messaging platforms. OpenClaw flows from user to local gateway to direct connectors to platform APIs." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;MindRoom uses Matrix as the federated backbone; OpenClaw takes a local-gateway, direct-connector approach.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;And then there&amp;rsquo;s &lt;a href="https://moltbook.com" target="_blank" rel="noopener"&gt;Moltbook&lt;/a&gt;—a social network built exclusively for AI agents that the internet was losing its mind over for a couple of days.&#10;I&amp;rsquo;m not personally convinced it&amp;rsquo;s the next big thing, but what caught my attention is that MindRoom could support something like it &lt;em&gt;natively&lt;/em&gt;.&#10;Matrix is already federated—agents on different servers can already interact, join shared rooms, and collaborate across organizational boundaries.&#10;A bridge from a Reddit-like platform to Matrix and your agents could participate without any special integration.&lt;/p&gt;&#10;&lt;p&gt;Building agent-to-agent infrastructure from scratch also means building security from scratch—and that&amp;rsquo;s where new platforms tend to struggle.&#10;Matrix has spent years hardening its E2E encryption, and it&amp;rsquo;s already deployed by governments and healthcare organizations at scale.&#10;Building on that foundation means you inherit those security properties rather than hoping to get them right yourself.&lt;/p&gt;&#10;&lt;h2 id="12-whats-next"&gt;12. What&amp;rsquo;s next &lt;a class="anchor" href="#12-whats-next" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I first drafted this post a couple of months ago, when MindRoom was still something I was trying to come back to.&#10;That part is already out of date.&#10;Since January I&amp;rsquo;ve been spending nearly every free hour on it again, and since March it has effectively been my full-time work.&#10;The codebase supports 8+ AI model providers—OpenAI, Anthropic, Ollama, Groq, Google, OpenRouter, DeepSeek, Cerebras—and the core architecture is solid.&#10;It also has real users now.&#10;My rough guess is that something like 30-100 people have tried MindRoom in one form or another, which is still early, but meaningfully different from &amp;ldquo;only I have touched this.&amp;rdquo;&#10;There&amp;rsquo;s already a Docker Compose file and a &lt;a href="https://github.com/mindroom-ai/mindroom-stack" target="_blank" rel="noopener"&gt;starter repository&lt;/a&gt; to get started, but I want to make it even simpler.&lt;/p&gt;&#10;&lt;p&gt;What I&amp;rsquo;m focusing on now:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Skills system&lt;/strong&gt;: Building an ecosystem of reusable agent behaviors (already partially implemented with OpenClaw-compatible format)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Even easier onboarding&lt;/strong&gt;: A &lt;code&gt;docker compose up&lt;/code&gt; gets you running today, but I want a wizard that gets you from zero to working agents without touching a YAML file&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Community&lt;/strong&gt;: Turning the small group of early users into a real operator and contributor community&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Scope discipline&lt;/strong&gt;: Keeping the work sustainable now that MindRoom is effectively full-time, not slipping back into the obsessive 2 AM marathon from before&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;m deliberately leaving the SaaS ambitions aside for now.&#10;The core system is what matters, and that&amp;rsquo;s where the energy is going.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re interested in AI agents that live in Matrix and work everywhere, check out &lt;a href="https://github.com/mindroom-ai/mindroom" target="_blank" rel="noopener"&gt;MindRoom on GitHub&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Sometimes seeing someone else succeed with a similar idea is enough to remember why you started building in the first place.&#10;And sometimes the most important lesson from a project isn&amp;rsquo;t technical—it&amp;rsquo;s learning when to step away and when to come back.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Are you running AI agents on any messaging platforms? Have you tried Matrix for anything beyond regular chat? I&amp;rsquo;d love to hear about your setups!&lt;/em&gt;&lt;/p&gt;&#10;&lt;h2 id="links-and-resources"&gt;Links and resources &lt;a class="anchor" href="#links-and-resources" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/mindroom-ai/mindroom" target="_blank" rel="noopener"&gt;MindRoom on GitHub&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/mindroom-ai/mindroom-stack" target="_blank" rel="noopener"&gt;MindRoom Stack (starter repository)&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://matrix.org/" target="_blank" rel="noopener"&gt;Matrix protocol&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://openclaw.ai" target="_blank" rel="noopener"&gt;OpenClaw&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://mem0.ai/" target="_blank" rel="noopener"&gt;Mem0 (memory system inspiration)&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/agno-agi/agno" target="_blank" rel="noopener"&gt;Agno framework&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/matty" target="_blank" rel="noopener"&gt;Matty (Matrix CLI client)&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://lucumr.pocoo.org/2026/1/18/agent-psychosis/" target="_blank" rel="noopener"&gt;Agent Psychosis: Are We Going Insane?&lt;/a&gt; by Armin Ronacher&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;My agentic coding post&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;My local AI journey&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;Coding from my phone&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Auto-installing extras at runtime so users never have to think about dependencies</title><link>https://www.nijho.lt/post/auto-install-extras/</link><pubDate>Wed, 11 Feb 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/auto-install-extras/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-from-one-command-to-a-dependency-maze"&gt;1. From one command to a dependency maze&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-why-just-use-extras-breaks-down"&gt;2. Why &amp;ldquo;just use extras&amp;rdquo; breaks down&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-decorator"&gt;3. The decorator&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-checking-without-importing"&gt;4. Checking without importing&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-the-metadata-bridge"&gt;5. The metadata bridge&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-three-environments-three-strategies"&gt;6. Three environments, three strategies&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#a-uv-tool-install--the-persistent-environment"&gt;A) &lt;code&gt;uv tool install&lt;/code&gt; — the persistent environment&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#b-uvx--the-ephemeral-environment"&gt;B) &lt;code&gt;uvx&lt;/code&gt; — the ephemeral environment&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#c-regular-virtualenv-pipx-or-system-install"&gt;C) Regular virtualenv, &lt;code&gt;pipx&lt;/code&gt;, or system install&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-preventing-infinite-re-execution-loops"&gt;7. Preventing infinite re-execution loops&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-what-users-actually-see"&gt;8. What users actually see&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-keeping-everything-in-sync"&gt;9. Keeping everything in sync&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#10-why-uv-made-this-practical"&gt;10. Why uv made this practical&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#11-tradeoffs"&gt;11. Tradeoffs&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#12-not-dependency-injection"&gt;12. Not dependency injection&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#13-conclusion"&gt;13. Conclusion&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-from-one-command-to-a-dependency-maze"&gt;1. From one command to a dependency maze &lt;a class="anchor" href="#1-from-one-command-to-a-dependency-maze" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; started as a simple thing.&#10;Voice in, text out.&#10;It connected to a &lt;a href="https://github.com/rhasspy/wyoming" target="_blank" rel="noopener"&gt;Wyoming&lt;/a&gt; Whisper server for speech-to-text and a Piper server for text-to-speech, basically a local, open-source Siri triggered from terminal hotkeys.&#10;I wrote about the early version in my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt; post.&lt;/p&gt;&#10;&lt;p&gt;And then, as tends to happen with side projects I use every day, I kept adding things.&lt;/p&gt;&#10;&lt;p&gt;First a RAG proxy with ChromaDB, so I could chat with my documents.&#10;Then a long-term memory system, so conversations could remember context across sessions.&#10;Server commands that run Kokoro TTS or Whisper locally, optimized for whatever hardware you have—Apple Silicon gets MLX, NVIDIA gets CTranslate2.&#10;Voice activity detection with Silero VAD.&#10;Audio speed adjustment.&#10;An &lt;a href="https://www.nijho.lt/post/parallel-agentic-coding/"&gt;agent orchestration system&lt;/a&gt;.&#10;&lt;code&gt;systemd&lt;/code&gt;/&lt;code&gt;launchd&lt;/code&gt; service management for running background daemons.&lt;/p&gt;&#10;&lt;p&gt;Each of these features pulled in wildly different dependency trees.&#10;&lt;code&gt;torch&lt;/code&gt; and &lt;code&gt;transformers&lt;/code&gt; for GPU-accelerated Whisper.&#10;&lt;code&gt;sounddevice&lt;/code&gt; and &lt;code&gt;numpy&lt;/code&gt; for microphone access.&#10;&lt;code&gt;chromadb&lt;/code&gt; and &lt;code&gt;onnxruntime&lt;/code&gt; for vector storage.&#10;&lt;code&gt;kokoro&lt;/code&gt; with its entire spacy pipeline for neural TTS.&lt;/p&gt;&#10;&lt;p&gt;Today the project has &lt;strong&gt;13 optional extras&lt;/strong&gt;.&#10;The tool became much more useful, but dependency UX got progressively worse with every feature.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I moved from &amp;ldquo;users choose extras upfront&amp;rdquo; to &lt;strong&gt;runtime optional dependency resolution&lt;/strong&gt;.&#10;Commands declare required extras with &lt;code&gt;@requires_extras(...)&lt;/code&gt;; if something is missing, &lt;code&gt;agent-cli&lt;/code&gt; installs it in the right environment and re-runs the command automatically.&#10;The user just types &lt;code&gt;agent-cli transcribe&lt;/code&gt; and everything works.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-why-just-use-extras-breaks-down"&gt;2. Why &amp;ldquo;just use extras&amp;rdquo; breaks down &lt;a class="anchor" href="#2-why-just-use-extras-breaks-down" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Python&amp;rsquo;s &lt;code&gt;optional-dependencies&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt; is the standard mechanism for this, and it&amp;rsquo;s the right packaging primitive.&#10;You define groups, users install with &lt;code&gt;pip install agent-cli[rag]&lt;/code&gt;.&#10;On paper, this is perfectly reasonable.&lt;/p&gt;&#10;&lt;p&gt;In practice, it falls apart when you have 13 of them:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;optional-dependencies&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;audio&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;numpy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sounddevice&amp;gt;=0.4.6&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;agent-cli[wyoming]&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;llm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pydantic-ai-slim[openai,google,duckduckgo,vertexai]&amp;gt;=0.1.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;rag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pydantic-ai-slim[openai,google]&amp;gt;=0.1.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;chromadb&amp;gt;=0.4.22&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pydantic-ai-slim[openai,google]&amp;gt;=0.1.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;chromadb&amp;gt;=0.4.22&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;vad&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;onnxruntime&amp;gt;=1.16.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;faster-whisper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;faster-whisper&amp;gt;=1.0.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mlx-whisper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;mlx-whisper&amp;gt;=0.4.0; sys_platform == &amp;#39;darwin&amp;#39; and platform_machine == &amp;#39;arm64&amp;#39;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;kokoro&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;kokoro&amp;gt;=0.9.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;soundfile&amp;gt;=0.12.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;piper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;piper-tts&amp;gt;=1.2.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ... and more&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The UX problem is obvious.&#10;If I want to transcribe voice to text, do I need &lt;code&gt;audio&lt;/code&gt;? &lt;code&gt;llm&lt;/code&gt;? &lt;code&gt;vad&lt;/code&gt;? All three?&#10;What if I want the Whisper server to run locally—is that &lt;code&gt;faster-whisper&lt;/code&gt; or &lt;code&gt;mlx-whisper&lt;/code&gt;?&#10;Do I also need &lt;code&gt;server&lt;/code&gt; for that?&lt;/p&gt;&#10;&lt;p&gt;Expecting users to study a dependency matrix before running a CLI command is a terrible experience.&#10;I wanted three things:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;A lightweight base install that&amp;rsquo;s fast and doesn&amp;rsquo;t pull in heavy ML dependencies.&lt;/li&gt;&#10;&lt;li&gt;Commands that &amp;ldquo;just work&amp;rdquo; when first used.&lt;/li&gt;&#10;&lt;li&gt;A clear opt-out for people who don&amp;rsquo;t want automatic installs.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="3-the-decorator"&gt;3. The decorator &lt;a class="anchor" href="#3-the-decorator" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The core idea is simple.&#10;Each CLI command declares what it needs, and the decorator handles the rest:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;transcribe&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rich_help_panel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Voice Commands&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;audio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;llm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it from the command author&amp;rsquo;s perspective.&#10;One line, and you never think about dependency management in that command again.&lt;/p&gt;&#10;&lt;p&gt;Here are a few more examples from the codebase to show how this scales:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Continuous transcription needs audio, voice activity detection, and an LLM&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;audio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vad&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;llm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe_live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# TTS server needs the server framework + one of two TTS backends + Wyoming protocol&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;piper|kokoro&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wyoming&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_tts_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Whisper server: any of three ASR backends will do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;faster-whisper|mlx-whisper|whisper-transformers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wyoming&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_whisper_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice the pipe syntax: &lt;code&gt;&amp;quot;piper|kokoro&amp;quot;&lt;/code&gt; means &amp;ldquo;you need &lt;em&gt;either&lt;/em&gt; Piper &lt;em&gt;or&lt;/em&gt; Kokoro—any one of these satisfies the requirement.&amp;rdquo;&#10;On Apple Silicon, if you already have &lt;code&gt;mlx-whisper&lt;/code&gt; installed, it won&amp;rsquo;t try to install &lt;code&gt;faster-whisper&lt;/code&gt; instead.&lt;/p&gt;&#10;&lt;p&gt;The &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L365-L383" target="_blank" rel="noopener"&gt;decorator itself&lt;/a&gt; is straightforward:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@functools.wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_check_and_install_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decorator&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The real complexity lives in &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L321-L359" target="_blank" rel="noopener"&gt;&lt;code&gt;_check_and_install_extras&lt;/code&gt;&lt;/a&gt;, which handles several installation scenarios.&#10;But the interface for command authors is always a single line.&lt;/p&gt;&#10;&lt;h2 id="4-checking-without-importing"&gt;4. Checking without importing &lt;a class="anchor" href="#4-checking-without-importing" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You don&amp;rsquo;t want to &lt;em&gt;import&lt;/em&gt; &lt;code&gt;torch&lt;/code&gt; just to check if it&amp;rsquo;s available—that takes seconds and loads hundreds of megabytes into memory.&#10;Instead, I use &lt;code&gt;importlib.util.find_spec&lt;/code&gt;, which checks whether a module exists on the import path without executing any of its code.&#10;No heavy imports, no GPU initialization, no side effects.&#10;The check is essentially free.&lt;/p&gt;&#10;&lt;h2 id="5-the-metadata-bridge"&gt;5. The metadata bridge &lt;a class="anchor" href="#5-the-metadata-bridge" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;There&amp;rsquo;s a gap between what &lt;code&gt;pyproject.toml&lt;/code&gt; knows and what the runtime check needs.&#10;&lt;code&gt;pyproject.toml&lt;/code&gt; says the &lt;code&gt;rag&lt;/code&gt; extra requires &lt;code&gt;chromadb&amp;gt;=0.4.22&lt;/code&gt;—that&amp;rsquo;s the &lt;em&gt;pip package name&lt;/em&gt;.&#10;But at runtime, I need to check &lt;em&gt;Python import names&lt;/em&gt;.&#10;Sometimes these are the same (&lt;code&gt;chromadb&lt;/code&gt; → &lt;code&gt;import chromadb&lt;/code&gt;), but often they&amp;rsquo;re not: &lt;code&gt;pydantic-ai-slim&lt;/code&gt; is imported as &lt;code&gt;pydantic_ai&lt;/code&gt;, &lt;code&gt;piper-tts&lt;/code&gt; as &lt;code&gt;piper&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;To bridge this, I generate an &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/_extras.json" target="_blank" rel="noopener"&gt;&lt;code&gt;_extras.json&lt;/code&gt;&lt;/a&gt; file that maps each extra to its description and the Python import names needed for the runtime check:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;audio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Audio recording/playback&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;sounddevice&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;rag&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;RAG proxy (ChromaDB, embeddings)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;chromadb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pydantic_ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Long-term memory proxy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;chromadb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pydantic_ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;vad&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Voice Activity Detection (Silero VAD via ONNX)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;onnxruntime&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;faster-whisper&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Whisper ASR via CTranslate2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;faster_whisper&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This file is &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/sync_extras.py" target="_blank" rel="noopener"&gt;generated by a script&lt;/a&gt; that parses &lt;code&gt;pyproject.toml&lt;/code&gt; and applies a mapping table for the package-name-to-import-name conversions.&#10;A pre-commit hook ensures it stays in sync—if you add an extra to &lt;code&gt;pyproject.toml&lt;/code&gt; and forget to update the metadata, the commit fails.&lt;/p&gt;&#10;&lt;p&gt;I wish Python packaging had a standard way to discover a package&amp;rsquo;s top-level import name from its distribution name.&#10;It doesn&amp;rsquo;t, so the mapping table is the least-bad option I found.&lt;/p&gt;&#10;&lt;h2 id="6-three-environments-three-strategies"&gt;6. Three environments, three strategies &lt;a class="anchor" href="#6-three-environments-three-strategies" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The hard part is that people run &lt;code&gt;agent-cli&lt;/code&gt; in several different ways, and each needs a different installation strategy.&lt;/p&gt;&#10;&lt;h3 id="a-uv-tool-install--the-persistent-environment"&gt;A) &lt;code&gt;uv tool install&lt;/code&gt; — the persistent environment &lt;a class="anchor" href="#a-uv-tool-install--the-persistent-environment" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;When installed via &lt;code&gt;uv tool install agent-cli&lt;/code&gt;, the tool lives in a persistent environment under &lt;code&gt;~/.local/share/uv/tools/agent-cli/&lt;/code&gt;.&#10;This environment has a &lt;code&gt;uv-receipt.toml&lt;/code&gt; file that tracks which extras are installed.&lt;/p&gt;&#10;&lt;p&gt;When auto-install triggers, the system reads the current extras from that receipt, merges in the new ones, and reinstalls:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;install_extras_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quiet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_uv_tool_install&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;current_extras&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_current_uv_tool_extras&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_extras&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_extras&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_install_via_uv_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_extras&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quiet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;quiet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So if you already had &lt;code&gt;audio&lt;/code&gt; installed and now run a command that needs &lt;code&gt;llm&lt;/code&gt;, it becomes &lt;code&gt;uv tool install agent-cli[audio,llm] --force&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The key insight: &lt;code&gt;uv tool upgrade&lt;/code&gt; reads &lt;code&gt;uv-receipt.toml&lt;/code&gt; and preserves the extras list.&#10;Once auto-install adds an extra, it persists across future upgrades.&#10;You never have to install it again.&lt;/p&gt;&#10;&lt;p&gt;(&lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L179-L235" target="_blank" rel="noopener"&gt;Full implementation&lt;/a&gt;)&lt;/p&gt;&#10;&lt;h3 id="b-uvx--the-ephemeral-environment"&gt;B) &lt;code&gt;uvx&lt;/code&gt; — the ephemeral environment &lt;a class="anchor" href="#b-uvx--the-ephemeral-environment" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;uvx&lt;/code&gt; is uv&amp;rsquo;s tool for running packages in temporary, disposable environments.&#10;Running &lt;code&gt;uvx agent-cli transcribe&lt;/code&gt; creates a fresh environment, installs &lt;code&gt;agent-cli&lt;/code&gt;, runs the command, and throws the environment away.&lt;/p&gt;&#10;&lt;p&gt;The problem: there&amp;rsquo;s nowhere to persist extras.&#10;No &lt;code&gt;uv-receipt.toml&lt;/code&gt; to update, no virtualenv that survives the invocation.&lt;/p&gt;&#10;&lt;p&gt;The solution is one of my favorite tricks in this system: re-execute the entire command with the extras baked into the &lt;code&gt;uvx&lt;/code&gt; invocation itself:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_maybe_reexec_with_uvx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;_is_uvx_cache&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;uvx_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uvx&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;uvx_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;extras_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uvx_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--python&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.13&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;agent-cli[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;extras_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_maybe_exec_with_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Re-running with extras: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;extras_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So &lt;code&gt;uvx agent-cli transcribe&lt;/code&gt; transparently becomes &lt;code&gt;uvx agent-cli[audio,llm] transcribe&lt;/code&gt;.&#10;The user sees a brief &amp;ldquo;Re-running with extras: audio, llm&amp;rdquo; message, and then the command works.&lt;/p&gt;&#10;&lt;p&gt;Environment detection is straightforward—&lt;code&gt;uvx&lt;/code&gt; environments live under the uv cache directory:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_uvx_cache&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;prefix_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_posix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/cache/uv/&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prefix_str&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/archive-v&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prefix_str&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(&lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L287-L301" target="_blank" rel="noopener"&gt;Full implementation&lt;/a&gt;)&lt;/p&gt;&#10;&lt;h3 id="c-regular-virtualenv-pipx-or-system-install"&gt;C) Regular virtualenv, &lt;code&gt;pipx&lt;/code&gt;, or system install &lt;a class="anchor" href="#c-regular-virtualenv-pipx-or-system-install" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;For plain &lt;code&gt;pip install agent-cli&lt;/code&gt;, &lt;code&gt;pipx&lt;/code&gt;, or virtualenv installs, the system falls back to direct installation.&#10;Each extra has a pre-generated requirements file with pinned versions (e.g., &lt;code&gt;_requirements/audio.txt&lt;/code&gt;), and the install command adapts to whatever&amp;rsquo;s available:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_install_cmd&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;in_venv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_prefix&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uv&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uv&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pip&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;install&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--python&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;in_venv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--system&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pip&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;install&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;in_venv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Prefers &lt;code&gt;uv pip&lt;/code&gt; when available (fast), falls back to regular pip.&#10;Uses &lt;code&gt;--system&lt;/code&gt; or &lt;code&gt;--user&lt;/code&gt; as appropriate when not in a virtualenv.&#10;This is also the path that &lt;code&gt;pipx&lt;/code&gt; installations take—&lt;code&gt;pipx&lt;/code&gt; creates a regular virtualenv under the hood, so the auto-installer detects it as a venv and installs directly into it.&lt;/p&gt;&#10;&lt;p&gt;(&lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L215-L248" target="_blank" rel="noopener"&gt;Full implementation&lt;/a&gt;)&lt;/p&gt;&#10;&lt;h2 id="7-preventing-infinite-re-execution-loops"&gt;7. Preventing infinite re-execution loops &lt;a class="anchor" href="#7-preventing-infinite-re-execution-loops" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;There&amp;rsquo;s an obvious footgun here: what if the install succeeds but the package still isn&amp;rsquo;t importable for some reason?&#10;You&amp;rsquo;d get an infinite loop of install → re-exec → check → install → re-exec → &amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;The fix is a simple environment variable marker:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_AGENT_CLI_REEXEC&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_maybe_exec_with_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c1"&gt;# Already re-executed once, don&amp;#39;t loop&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execvpe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;os.execvpe&lt;/code&gt; replaces the current process entirely—it never returns.&#10;The marker ensures we only do this once.&#10;If the second run still can&amp;rsquo;t find the packages, it falls back to a helpful error message instead of looping forever.&lt;/p&gt;&#10;&lt;h2 id="8-what-users-actually-see"&gt;8. What users actually see &lt;a class="anchor" href="#8-what-users-actually-see" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With auto-install &lt;strong&gt;enabled&lt;/strong&gt; (the default), a first run looks like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Auto-installing missing extras: audio, llm&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Running: uv tool install agent-cli[audio,llm] --force --python 3.13 -q&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Installation complete!&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Re-running with installed extras...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then the command runs normally.&#10;Next time, it runs instantly—no install needed.&lt;/p&gt;&#10;&lt;p&gt;With auto-install &lt;strong&gt;disabled&lt;/strong&gt;, the user gets a clear message telling them exactly what to do:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This command requires the &amp;#39;audio&amp;#39; extra (Audio recording/playback).&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Install with:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; uv tool install -p 3.13 &amp;#34;agent-cli\[audio]&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # or&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; agent-cli install-extras audio&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you prefer explicit control, there&amp;rsquo;s also a manual &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/install/extras.py#L24-L104" target="_blank" rel="noopener"&gt;&lt;code&gt;install-extras&lt;/code&gt;&lt;/a&gt; command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli install-extras rag memory &lt;span class="c1"&gt;# Install specific extras&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli install-extras --all &lt;span class="c1"&gt;# Install everything&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli install-extras --list &lt;span class="c1"&gt;# Show available extras with descriptions&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can opt out via environment variable or config file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;AGENT_CLI_NO_AUTO_INSTALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ~/.config/agent-cli/config.toml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;auto_install_extras&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="9-keeping-everything-in-sync"&gt;9. Keeping everything in sync &lt;a class="anchor" href="#9-keeping-everything-in-sync" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The runtime system depends on two generated artifacts:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;_extras.json&lt;/code&gt;&lt;/strong&gt;: maps extras to descriptions and import names (for the &lt;code&gt;find_spec&lt;/code&gt; check)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;_requirements/*.txt&lt;/code&gt;&lt;/strong&gt;: pinned dependency files for each extra (for the pip/uv install)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;code&gt;_extras.json&lt;/code&gt; is generated by &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/sync_extras.py" target="_blank" rel="noopener"&gt;&lt;code&gt;sync_extras.py&lt;/code&gt;&lt;/a&gt; (run manually or in CI).&#10;The requirements files are generated by &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/sync_requirements.py" target="_blank" rel="noopener"&gt;&lt;code&gt;sync_requirements.py&lt;/code&gt;&lt;/a&gt;, which runs as a pre-commit hook whenever &lt;code&gt;pyproject.toml&lt;/code&gt; or &lt;code&gt;uv.lock&lt;/code&gt; changes.&#10;A &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/check_extras_sync.py" target="_blank" rel="noopener"&gt;check script&lt;/a&gt; enforces that &lt;code&gt;_extras.json&lt;/code&gt; stays consistent with &lt;code&gt;pyproject.toml&lt;/code&gt;—if you add an extra but forget to update the metadata, the commit fails with a clear error.&lt;/p&gt;&#10;&lt;p&gt;This means the runtime system never drifts from the source of truth.&#10;&lt;code&gt;pyproject.toml&lt;/code&gt; is the canonical definition, everything else is derived.&lt;/p&gt;&#10;&lt;h2 id="10-why-uv-made-this-practical"&gt;10. Why uv made this practical &lt;a class="anchor" href="#10-why-uv-made-this-practical" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I want to be clear: this system would be &lt;em&gt;much&lt;/em&gt; harder without &lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;uv&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uv tool install&lt;/code&gt;&lt;/strong&gt; with persistent extras in &lt;code&gt;uv-receipt.toml&lt;/code&gt;—extras survive upgrades automatically&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uvx&lt;/code&gt;&lt;/strong&gt; for ephemeral execution with extras specified inline&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uv pip install&lt;/code&gt;&lt;/strong&gt; that&amp;rsquo;s fast enough for runtime auto-install to feel acceptable&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uv export&lt;/code&gt;&lt;/strong&gt; for generating pinned requirements files from &lt;code&gt;uv.lock&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;With traditional pip, auto-installing at runtime would take 30-60 seconds and feel terrible.&#10;With uv, it typically takes a second.&#10;That&amp;rsquo;s the difference between &amp;ldquo;annoying&amp;rdquo; and &amp;ldquo;barely noticeable.&amp;rdquo;&lt;/p&gt;&#10;&lt;h2 id="11-tradeoffs"&gt;11. Tradeoffs &lt;a class="anchor" href="#11-tradeoffs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This approach works well, but it&amp;rsquo;s still a tradeoff:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;There&amp;rsquo;s metadata to maintain.&lt;/strong&gt; The package-name-to-import-name mapping requires a lookup table with special cases like &lt;code&gt;piper-tts&lt;/code&gt; → &lt;code&gt;piper&lt;/code&gt; and &lt;code&gt;pydantic-ai-slim&lt;/code&gt; → &lt;code&gt;pydantic_ai&lt;/code&gt;. I wish &lt;code&gt;pyproject.toml&lt;/code&gt; had a field for this.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;The check only verifies importability, not version compatibility.&lt;/strong&gt; &lt;code&gt;find_spec&lt;/code&gt; tells you a module exists, not whether it&amp;rsquo;s the right version. In practice this hasn&amp;rsquo;t been a problem because uv&amp;rsquo;s pinned requirements handle versions, but it&amp;rsquo;s a theoretical gap.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Auto-install can fail in restricted environments.&lt;/strong&gt; Corporate machines, read-only containers, air-gapped systems—the opt-out exists for a reason.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Re-exec is a CLI pattern, not a library pattern.&lt;/strong&gt; &lt;code&gt;os.execvpe&lt;/code&gt; replaces the process, which means any shell hooks or signal handlers set up before the decorator runs are lost. Fine for CLI tools, would be problematic for libraries.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="12-not-dependency-injection"&gt;12. Not dependency injection &lt;a class="anchor" href="#12-not-dependency-injection" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I originally called this &amp;ldquo;dependency injection&amp;rdquo; in my head, but that&amp;rsquo;s the wrong term entirely.&#10;Dependency injection is about providing object dependencies through constructors or function parameters.&#10;This is something different.&lt;/p&gt;&#10;&lt;p&gt;The best names I&amp;rsquo;ve come up with:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;runtime optional dependency resolution&lt;/strong&gt; — accurate but a mouthful&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;on-demand extras installation&lt;/strong&gt; — emphasizes the user experience&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;lazy extras&lt;/strong&gt; — short, probably what I&amp;rsquo;d use in conversation&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If there&amp;rsquo;s a standard term for this pattern, I haven&amp;rsquo;t found it.&lt;/p&gt;&#10;&lt;h2 id="13-conclusion"&gt;13. Conclusion &lt;a class="anchor" href="#13-conclusion" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The underlying principle is simple: &lt;strong&gt;the tool should know what it needs, and it should get it&lt;/strong&gt;.&#10;Users shouldn&amp;rsquo;t have to study a matrix of 13 optional extras to figure out which pip command to run.&#10;They should just type the command they want, and it should work.&lt;/p&gt;&#10;&lt;p&gt;uv&amp;rsquo;s speed and tooling model made this practical.&#10;A decorator made it ergonomic for command authors.&#10;Pre-commit hooks made it maintainable.&#10;And three environment-detection strategies made it work regardless of how the user installed the tool.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re building a Python CLI with optional features that keep growing, consider this pattern.&#10;Your users will thank you, mostly by never having to ask &amp;ldquo;which extras do I need?&amp;rdquo;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;&lt;em&gt;The code discussed here is part of &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;, an open-source suite of AI-powered command-line tools.&#10;The implementation lives primarily in &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py" target="_blank" rel="noopener"&gt;&lt;code&gt;agent_cli/core/deps.py&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>Parallel agentic coding made trivial with `agent-cli dev`</title><link>https://www.nijho.lt/post/parallel-agentic-coding/</link><pubDate>Tue, 13 Jan 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/parallel-agentic-coding/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-the-just-use-worktrees-problem"&gt;1. The &amp;ldquo;just use worktrees&amp;rdquo; problem&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-why-parallel-development-matters-for-agentic-coding"&gt;2. Why parallel development matters for agentic coding&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-one-command-to-rule-them-all"&gt;3. One command to rule them all&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-it-knows-your-stack"&gt;4. It knows your stack&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-terminal-magic"&gt;5. Terminal magic&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-spawning-agents-with-prompts"&gt;6. Spawning agents with prompts&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-teaching-claude-to-spawn-agents"&gt;7. Teaching Claude to spawn agents&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-my-actual-workflow"&gt;8. My actual workflow&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-managing-environments"&gt;9. Managing environments&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#10-getting-started"&gt;10. Getting started&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#links"&gt;Links&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-the-just-use-worktrees-problem"&gt;1. The &amp;ldquo;just use worktrees&amp;rdquo; problem &lt;a class="anchor" href="#1-the-just-use-worktrees-problem" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Last week, I was chatting with a friend about my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic coding workflow&lt;/a&gt;.&#10;I was sharing some of my best practices, and when the topic of parallel development came up, I said:&lt;/p&gt;&#10;&lt;p&gt;&amp;ldquo;Just use git worktrees with quickly installable environments.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;He stared at me blankly.&lt;/p&gt;&#10;&lt;p&gt;And then it hit me: for most developers who just &lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt; and don&amp;rsquo;t touch their environment setup once it works, this is &lt;em&gt;not&lt;/em&gt; trivial.&#10;What seems obvious to me involves:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Understanding what git worktrees even are&lt;/li&gt;&#10;&lt;li&gt;Knowing the &lt;code&gt;git worktree add&lt;/code&gt; command and its flags&lt;/li&gt;&#10;&lt;li&gt;Remembering to fetch before creating&lt;/li&gt;&#10;&lt;li&gt;Creating a sibling directory structure&lt;/li&gt;&#10;&lt;li&gt;Running your package manager&amp;rsquo;s install command&lt;/li&gt;&#10;&lt;li&gt;Copying over &lt;code&gt;.env&lt;/code&gt; files&lt;/li&gt;&#10;&lt;li&gt;Setting up direnv or manually activating your virtual environment&lt;/li&gt;&#10;&lt;li&gt;Opening a new terminal tab&lt;/li&gt;&#10;&lt;li&gt;Navigating to the worktree&lt;/li&gt;&#10;&lt;li&gt;Finally launching your coding agent&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;That&amp;rsquo;s a lot of friction for something you want to do &lt;em&gt;frequently&lt;/em&gt; when using agentic tools.&lt;/p&gt;&#10;&lt;p&gt;So I built &lt;code&gt;agent-cli dev&lt;/code&gt;.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;code&gt;agent-cli dev new --agent --prompt &amp;quot;Fix the login bug&amp;quot;&lt;/code&gt; creates a complete, isolated development environment in one command.&#10;It creates a git worktree, installs dependencies, copies env files, sets up direnv, and opens your AI coding agent in a new terminal tab with your prompt already loaded—all automatically.&#10;No more manual setup, no more excuses to not work in parallel.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-why-parallel-development-matters-for-agentic-coding"&gt;2. Why parallel development matters for agentic coding &lt;a class="anchor" href="#2-why-parallel-development-matters-for-agentic-coding" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;previous post on agentic coding&lt;/a&gt;, I described my workflow of running 5-6 Claude Code sessions simultaneously, each working on a different feature.&#10;This is how I can easily work on multiple features in parallel while waiting for an agent to process and generate code.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The usual way:&lt;/strong&gt;&#10;Start feature A → wait for Claude to finish → review → move to feature B → repeat.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The better way:&lt;/strong&gt;&#10;Start feature A in tab 1 → switch to tab 2, start feature B while A is working → switch to tab 3, start feature C → circle back to tab 1, review what Claude did → give feedback, move to next tab → repeat every 10-15 minutes.&lt;/p&gt;&#10;&lt;p&gt;This parallel approach allows me to go much faster.&#10;But it only works if each tab has its own &lt;strong&gt;completely isolated and working environment&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s where &lt;a href="https://www.nijho.lt/post/git-worktree/"&gt;git worktrees&lt;/a&gt; come in.&#10;A worktree gives you a separate working directory for a branch, sharing the same git history but with independent file states.&lt;/p&gt;&#10;&lt;p&gt;The problem is the &lt;em&gt;setup&lt;/em&gt;.&#10;Every time you want to start a new feature:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# The manual way (don&amp;#39;t do this)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git fetch origin&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git worktree add ../my-project-worktrees/cool-feature -b cool-feature origin/main&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../my-project-worktrees/cool-feature&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv sync --all-extras &lt;span class="c1"&gt;# or npm install, or poetry install, or...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cp ../.env .env&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Now open a new terminal tab...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Navigate there...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Finally start claude&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After the 100th(?) time doing this manually, I knew I needed to automate it.&lt;/p&gt;&#10;&lt;h2 id="3-one-command-to-rule-them-all"&gt;3. One command to rule them all &lt;a class="anchor" href="#3-one-command-to-rule-them-all" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s what I built:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev new --agent&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it.&#10;One command.&#10;Here&amp;rsquo;s what it looks like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Generated branch name: cool-crane&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Creating worktree &lt;span class="k"&gt;for&lt;/span&gt; branch &lt;span class="s1"&gt;&amp;#39;cool-crane&amp;#39;&lt;/span&gt;...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Running: git fetch origin&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Running: git worktree add -b cool-crane ~/Work/project-worktrees/cool-crane origin/main&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ Created worktree at ~/Work/project-worktrees/cool-crane&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ Copied env file&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt;: .env.example, .envrc&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Detected Python project with uv&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Running: uv sync --all-extras&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ Running: direnv allow&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ direnv: allowed existing .envrc&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✓ Started claude in new iterm2 tab&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;╭─────────────────────────────────────────────── Success ───────────────────────────────────────────────╮&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Dev environment created: ~/Work/project-worktrees/cool-crane │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ Branch: cool-crane │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;4 seconds later, I have a new terminal tab with Claude Code already running in a fresh, isolated environment.&#10;Ready to work on my next feature.&lt;/p&gt;&#10;&lt;p&gt;What happens under the hood:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Generates a random branch name&lt;/strong&gt; like &lt;code&gt;clever-fox&lt;/code&gt; (you can just set the name yourself of course)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Creates the git worktree&lt;/strong&gt; in a sibling directory&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Detects the project type&lt;/strong&gt; and runs the appropriate setup (&lt;code&gt;uv sync&lt;/code&gt;, &lt;code&gt;npm install&lt;/code&gt;, etc.)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Copies &lt;code&gt;.env&lt;/code&gt; files&lt;/strong&gt; from the main repo&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Sets up direnv&lt;/strong&gt; so the environment activates automatically&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Opens a new terminal tab&lt;/strong&gt; (works with tmux, zellij, iTerm2, and more)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Launches your coding agent&lt;/strong&gt; in that tab&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The Docker-style random names (&lt;code&gt;clever-fox&lt;/code&gt;, &lt;code&gt;happy-bear&lt;/code&gt;, &lt;code&gt;swift-owl&lt;/code&gt;) there just for those too lazy to pick a name.&#10;Just &lt;code&gt;agent-cli dev new --agent&lt;/code&gt; and start coding.&lt;/p&gt;&#10;&lt;h2 id="4-it-knows-your-stack"&gt;4. It knows your stack &lt;a class="anchor" href="#4-it-knows-your-stack" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The command auto-detects your project type:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;If it sees&amp;hellip;&lt;/th&gt;&#10;&lt;th&gt;It runs&amp;hellip;&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;uv.lock&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;uv sync --all-extras&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;pixi.lock&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;pixi install&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;poetry.lock&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;poetry install&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;requirements.yaml&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;unidep install -e .&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;package-lock.json&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;cargo build&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;go.mod&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;go mod download&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;For my projects using &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;unidep&lt;/a&gt;, it even detects monorepos and adjusts the command accordingly.&lt;/p&gt;&#10;&lt;p&gt;It also generates the right &lt;code&gt;.envrc&lt;/code&gt; for &lt;a href="https://direnv.net/" target="_blank" rel="noopener"&gt;direnv&lt;/a&gt;.&#10;For a Python project with uv, that&amp;rsquo;s &lt;code&gt;source .venv/bin/activate&lt;/code&gt;.&#10;For a conda/unidep project, it&amp;rsquo;s the full micromamba activation.&#10;For Nix projects, it&amp;rsquo;s &lt;code&gt;use flake&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;This means when you &lt;code&gt;cd&lt;/code&gt; into the worktree later, your environment is automatically activated.&#10;No more &lt;code&gt;source .venv/bin/activate&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="5-terminal-magic"&gt;5. Terminal magic &lt;a class="anchor" href="#5-terminal-magic" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The real magic is opening a new terminal tab with the agent already running.&lt;/p&gt;&#10;&lt;p&gt;The command detects which terminal you&amp;rsquo;re in and uses the appropriate API—whether that&amp;rsquo;s iTerm2, Kitty, Warp, or GNOME Terminal.&#10;It even works with terminal multiplexers like &lt;strong&gt;tmux&lt;/strong&gt; and &lt;strong&gt;zellij&lt;/strong&gt; (my current favorite), opening a new window/tab in your existing session.&lt;/p&gt;&#10;&lt;h2 id="6-spawning-agents-with-prompts"&gt;6. Spawning agents with prompts &lt;a class="anchor" href="#6-spawning-agents-with-prompts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The &lt;code&gt;--prompt&lt;/code&gt; option transforms how I spawn parallel agents.&#10;Instead of just opening a blank Claude session, I can give the new agent its task immediately:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev new fix-auth-bug --agent --prompt &lt;span class="s2"&gt;&amp;#34;Fix the login validation bug in auth.py. The issue is that empty passwords pass validation.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The agent opens with the prompt already loaded and starts working immediately.&#10;No copy-pasting, no context switching, no &amp;ldquo;let me explain what I need you to do.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;For longer, more detailed prompts (which you should use for complex tasks), there&amp;rsquo;s &lt;code&gt;--prompt-file&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev new new-feature --agent --prompt-file .claude/spawn-prompt.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is particularly powerful when you&amp;rsquo;re in a Claude session and discover a feature that needs work but is unrelated to your current task.&#10;Instead of context-switching or noting it for later, you can spawn a new agent right there:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Write a detailed prompt to a file (Claude can do this)&lt;/li&gt;&#10;&lt;li&gt;Run &lt;code&gt;agent-cli dev new unrelated-feature --agent --prompt-file .claude/spawn-prompt.md&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;A new terminal tab opens with a fresh Claude session already working on the task&lt;/li&gt;&#10;&lt;li&gt;Continue with your original work&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The spawned agent works independently in its own branch while you stay focused.&lt;/p&gt;&#10;&lt;h2 id="7-teaching-claude-to-spawn-agents"&gt;7. Teaching Claude to spawn agents &lt;a class="anchor" href="#7-teaching-claude-to-spawn-agents" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s where it gets really interesting: you can teach Claude Code itself how to spawn parallel agents.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev install-skill&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This installs a &lt;a href="https://docs.anthropic.com/en/docs/claude-code/skills" target="_blank" rel="noopener"&gt;Claude Code skill&lt;/a&gt; that teaches Claude how to use &lt;code&gt;agent-cli dev&lt;/code&gt;.&#10;The skill includes detailed examples and prompt templates following Anthropic&amp;rsquo;s &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview" target="_blank" rel="noopener"&gt;prompt engineering guide&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;With the skill installed, you can say things like:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;Work on auth, payments, and notifications in parallel&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;This feature is unrelated to what we&amp;rsquo;re doing—spawn a new agent for it&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;Split this refactoring by module and work on them simultaneously&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Claude will write well-structured prompts (using proper XML tags, clear context, explicit requirements) and spawn the agents for you.&#10;Each spawned agent writes a report to &lt;code&gt;.claude/REPORT.md&lt;/code&gt; when done, so you can easily review what happened.&lt;/p&gt;&#10;&lt;p&gt;One of my favorite patterns: I ask Claude to look at the latest 10 open GitHub issues and spawn an agent for each one.&#10;Ten independent parallel environments, set up in seconds, each working on a different issue.&#10;That&amp;rsquo;s the kind of leverage this enables.&lt;/p&gt;&#10;&lt;h2 id="8-my-actual-workflow"&gt;8. My actual workflow &lt;a class="anchor" href="#8-my-actual-workflow" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s how I use this in practice.&#10;I&amp;rsquo;m working on &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;agent-cli&lt;/a&gt; itself, and I want to add three features:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tab 1: New RAG feature&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev new rag-improvements --agent --prompt &lt;span class="s2"&gt;&amp;#34;Improve RAG chunking strategy for better retrieval accuracy&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tab 2: Fix a bug in the transcribe command&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev new fix-transcribe-vad --agent --prompt &lt;span class="s2"&gt;&amp;#34;Fix VAD detection cutting off words at sentence boundaries&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tab 3: Refactor the config system&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev new config-refactor --agent --prompt &lt;span class="s2"&gt;&amp;#34;Refactor config.py to use a single Config class with lazy loading&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three commands, three isolated environments, three Claude Code sessions already working on their tasks.&lt;/p&gt;&#10;&lt;p&gt;I cycle through the tabs every 10-15 minutes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Review what Claude did&lt;/li&gt;&#10;&lt;li&gt;Give voice feedback using my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;transcription workflow&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Move to the next tab&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;When a feature is done, I have Claude open a PR from that branch.&#10;When the PR is merged:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev clean --merged&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This uses the GitHub CLI to check which branches have merged PRs, then removes those worktrees and deletes the branches.&#10;No more zombie worktrees cluttering my disk.&lt;/p&gt;&#10;&lt;h2 id="9-managing-environments"&gt;9. Managing environments &lt;a class="anchor" href="#9-managing-environments" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;A few other commands I use regularly:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# List all worktrees&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev list&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Navigate to a worktree&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;agent-cli dev path clever-fox&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Remove a worktree&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev rm clever-fox&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Force removal + delete branch&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev rm clever-fox --force --delete-branch&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check what&amp;#39;s available on your system&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev doctor&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;doctor&lt;/code&gt; command is handy for seeing which terminals, editors, and coding agents are installed and detected.&lt;/p&gt;&#10;&lt;h2 id="10-getting-started"&gt;10. Getting started &lt;a class="anchor" href="#10-getting-started" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli dev&lt;/code&gt; is part of the &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;agent-cli&lt;/a&gt; package:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# With uv (recommended)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv tool install agent-cli&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then in any git repository:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Check what&amp;#39;s available&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev doctor&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create your first parallel environment with a task&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev new my-feature --agent --prompt &lt;span class="s2"&gt;&amp;#34;Add user profile page with avatar upload&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or install the skill so Claude can spawn agents for you&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli dev install-skill&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Supported coding agents:&lt;/strong&gt; Claude Code, Codex, Gemini, Aider, GitHub Copilot, Continue, OpenCode, and Cursor Agent.&#10;Use &lt;code&gt;--with-agent gemini&lt;/code&gt; to pick a specific one, or set a default in your config.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Supported terminals:&lt;/strong&gt; tmux, zellij, iTerm2, Terminal.app, Kitty, Warp, and GNOME Terminal.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Supported editors:&lt;/strong&gt; Cursor, VS Code, Zed, PyCharm (and other JetBrains IDEs), Neovim, Vim, Emacs, Sublime Text, and nano.&lt;/p&gt;&#10;&lt;h2 id="conclusion"&gt;Conclusion &lt;a class="anchor" href="#conclusion" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The barrier between &amp;ldquo;I want to work on this feature&amp;rdquo; and &amp;ldquo;I&amp;rsquo;m actually working on it in an isolated environment&amp;rdquo; should be zero.&lt;/p&gt;&#10;&lt;p&gt;Before:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;2-5 minutes of manual setup per feature&lt;/li&gt;&#10;&lt;li&gt;Often skipped because &amp;ldquo;it&amp;rsquo;s not worth it for a small change&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Mental overhead remembering all the steps&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;After:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;One command, few seconds&lt;/li&gt;&#10;&lt;li&gt;No excuse not to work in isolation&lt;/li&gt;&#10;&lt;li&gt;More parallel features, faster iteration&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If you&amp;rsquo;re using agentic coding tools and not yet working in parallel, you&amp;rsquo;re leaving productivity on the table.&#10;And if the setup friction was stopping you—well, now it&amp;rsquo;s gone.&lt;/p&gt;&#10;&lt;p&gt;Try it out and let me know what you think!&#10;The code is open-source at &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;github.com/basnijholt/agent-cli&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="links"&gt;Links &lt;a class="anchor" href="#links" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;agent-cli on GitHub&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://agent-cli.nijho.lt/commands/dev/" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli dev&lt;/code&gt; documentation&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/claude-code/skills" target="_blank" rel="noopener"&gt;Claude Code skills&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/overview" target="_blank" rel="noopener"&gt;Anthropic prompt engineering guide&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/git-worktree/"&gt;My post on git worktrees&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;My post on agentic coding&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://zellij.dev/" target="_blank" rel="noopener"&gt;Zellij&lt;/a&gt; / &lt;a href="https://github.com/tmux/tmux" target="_blank" rel="noopener"&gt;tmux&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://direnv.net/" target="_blank" rel="noopener"&gt;direnv&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>SOTA RAG &amp; Memory without the database: Files, Git, and simple folders</title><link>https://www.nijho.lt/post/file-based-rag-memory/</link><pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/file-based-rag-memory/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-the-amnesia-problem"&gt;1. The amnesia problem&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-learning-from-failure-ai-journal"&gt;2. Learning from failure: AI Journal&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-how-i-studied-sota-clone-read-re-implement"&gt;3. How I studied SOTA: clone, read, re-implement&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-dependency-problem-why-onnx-over-pytorch"&gt;4. The dependency problem: why ONNX over PyTorch&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-rag-just-let-me-grep-it-kind-of"&gt;5. RAG: just let me grep it (kind of)&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-architecture-openai-compatibility"&gt;The architecture: OpenAI compatibility&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#two-stage-retrieval-the-sota-approach"&gt;Two-stage retrieval: the SOTA approach&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-memory-files-over-databases"&gt;6. Memory: files over databases&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#git-integration-versioning-memory-"&gt;Git integration: versioning memory 🕰️&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-reconciliation-loop"&gt;The reconciliation loop&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-tiny-detail-that-broke-everything"&gt;The tiny detail that broke everything&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-why-building-this-was-necessary"&gt;7. Why building this was necessary&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#privacy"&gt;Privacy&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#integration"&gt;Integration&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#dependencies"&gt;Dependencies&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-try-it-yourself"&gt;8. Try it yourself&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#try-memory"&gt;Try Memory&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#try-rag"&gt;Try RAG&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-what-i-learned"&gt;9. What I learned&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-the-amnesia-problem"&gt;1. The amnesia problem &lt;a class="anchor" href="#1-the-amnesia-problem" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;If you&amp;rsquo;ve read my posts on &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic coding&lt;/a&gt; or my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt;, you know I am all-in on using AI to control my computer and write software.&#10;But one critical piece was missing: &lt;strong&gt;Context&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;LLMs are powerful but stateless.&#10;Every time I start a new chat, the model forgets who I am, what I&amp;rsquo;m working on, and that I prefer &lt;a href="https://www.nijho.lt/post/functional-python/"&gt;functional Python code&lt;/a&gt; over object-oriented styles.&#10;Worse, it also forgets the thing I just told it five minutes ago in another window.&lt;/p&gt;&#10;&lt;p&gt;The standard solution to this is &lt;a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" target="_blank" rel="noopener"&gt;RAG (Retrieval-Augmented Generation)&lt;/a&gt; and &lt;a href="https://arxiv.org/abs/2310.08560" target="_blank" rel="noopener"&gt;Memory&lt;/a&gt;.&#10;But when I looked at the available tools, I hit a wall.&lt;/p&gt;&#10;&lt;p&gt;Most solutions fell into two buckets:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;SaaS / Web UIs:&lt;/strong&gt; Upload your files to a cloud service or use a specific web chat interface that locks you in.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Developer Libraries:&lt;/strong&gt; &amp;ldquo;Just use LangChain/LlamaIndex!&amp;rdquo; they say. Great, now I have to write a Python script every time I want to ask a question about a PDF?&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t want a library. I didn&amp;rsquo;t want a web UI.&#10;&lt;strong&gt;I wanted a folder.&lt;/strong&gt;&#10;I wanted to drop a file into &lt;code&gt;~/my-docs&lt;/code&gt;, and have my AI instantly know about it.&#10;And I wanted my AI&amp;rsquo;s &amp;ldquo;memory&amp;rdquo; to be a file I can edit, not a hidden vector in a database I can&amp;rsquo;t inspect.&lt;/p&gt;&#10;&lt;p&gt;So I decided to build my own file-based solution.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; features that combine two ideas:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;File-based storage:&lt;/strong&gt; Documents in a folder, memories as Markdown files with Git versioning.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;OpenAI-compatible proxy:&lt;/strong&gt; Works with &lt;em&gt;any&lt;/em&gt; OpenAI-compatible tool—Cursor, Cline, Open WebUI, LibreChat, Lobe Chat, your terminal—without lock-in.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Tools like Open WebUI have built-in RAG, but that forces you to use only Open WebUI.&#10;My proxy means I can switch tools whenever I want, and they all share the same memory and document index.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-learning-from-failure-ai-journal"&gt;2. Learning from failure: AI Journal &lt;a class="anchor" href="#2-learning-from-failure-ai-journal" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t my first attempt at building a memory system.&lt;/p&gt;&#10;&lt;p&gt;A few months ago, I built &lt;a href="https://github.com/basnijholt/aijournal" target="_blank" rel="noopener"&gt;&lt;strong&gt;AI Journal&lt;/strong&gt;&lt;/a&gt;—an ambitious local-first journaling app where an AI would read your entries and build a &amp;ldquo;living self-model&amp;rdquo; of who you are.&#10;It had sophisticated features: a structured &amp;ldquo;persona core,&amp;rdquo; typed &amp;ldquo;claim atoms&amp;rdquo; with evidence links, four organizational layers (L1→L4), recency-aware scoring with decay functions, and Git-based time travel to &amp;ldquo;ask questions of your younger self.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;The architecture seemed solid.&#10;The problem was simpler: &lt;strong&gt;local models aren&amp;rsquo;t good enough yet.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;I designed the whole thing to run entirely on Ollama with my dual RTX 3090s.&#10;But even with a 48GB of VRAM, the models couldn&amp;rsquo;t reliably extract structured claims, handle the multi-step reconciliation, or maintain coherence across long conversations.&#10;Whether the design was right, I still don&amp;rsquo;t know—but the execution definitely hit the wall of current local LLM capabilities.&lt;/p&gt;&#10;&lt;p&gt;The lesson: I tried to build the entire system at once without validating the core concepts and individual ideas first.&#10;This time, I took a different approach: &lt;strong&gt;tackle it problem by problem.&lt;/strong&gt;&#10;Instead of designing a complete system upfront, I would validate each piece—chunking, retrieval, re-ranking, memory reconciliation—before combining them.&#10;The experience and UX would be my own, but the algorithms would be adapted from established projects.&lt;/p&gt;&#10;&lt;h2 id="3-how-i-studied-sota-clone-read-re-implement"&gt;3. How I studied SOTA: clone, read, re-implement &lt;a class="anchor" href="#3-how-i-studied-sota-clone-read-re-implement" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach was more methodical this time.&#10;I cloned five major frameworks into the same workspace as &lt;code&gt;agent-cli&lt;/code&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/run-llama/llama_index" target="_blank" rel="noopener"&gt;LlamaIndex&lt;/a&gt;&lt;/strong&gt;: The OG RAG framework&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/langchain-ai/langchain" target="_blank" rel="noopener"&gt;LangChain&lt;/a&gt;&lt;/strong&gt;: The kitchen-sink approach&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/letta-ai/letta" target="_blank" rel="noopener"&gt;Letta&lt;/a&gt;&lt;/strong&gt; (formerly MemGPT): Stateful agents with long-term memory&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/mem0ai/mem0" target="_blank" rel="noopener"&gt;Mem0&lt;/a&gt;&lt;/strong&gt;: Focused specifically on memory extraction&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/pydantic/pydantic-ai" target="_blank" rel="noopener"&gt;PydanticAI&lt;/a&gt;&lt;/strong&gt;: Clean, typed agent framework&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t read through all this code myself—that would take months.&#10;Instead, I used AI coding agents to search the codebases and write summaries of the technical implementation details:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Gemini CLI&lt;/strong&gt; (Gemini 3.0 Pro)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; (Opus 4.5)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt; (GPT-5.1-Codex-Max)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I fed those summaries into &lt;strong&gt;ChatGPT 5 Pro&lt;/strong&gt; together with my high level plan of what I wanted to create, and it created an implementation plan.&#10;Then I took that plan back to the coding agents for implementation.&#10;I personally found Opus 4.5 the best coding model, but GPT‑5.1 Codex Max was the best at reviewing the code and actually identifying critical bugs.&lt;/p&gt;&#10;&lt;p&gt;For validation, I used &lt;a href="https://github.com/basnijholt/clip-files" target="_blank" rel="noopener"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt; to copy the agent-cli source code plus the architecture documents (like &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/memory.md" target="_blank" rel="noopener"&gt;memory.md&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/rag.md" target="_blank" rel="noopener"&gt;rag.md&lt;/a&gt;) into ChatGPT 5 Pro to validate the approach.&lt;/p&gt;&#10;&lt;p&gt;The AI did the searching and implementing; I did the understanding and decision-making.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Existing frameworks are incredibly over-engineered for my use case, but the &lt;em&gt;core algorithms&lt;/em&gt; are gold.&#10;My job was to extract the essence and re-implement it cleanly.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="4-the-dependency-problem-why-onnx-over-pytorch"&gt;4. The dependency problem: why ONNX over PyTorch &lt;a class="anchor" href="#4-the-dependency-problem-why-onnx-over-pytorch" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One thing became immediately clear when going through these frameworks: &lt;strong&gt;they&amp;rsquo;re massive&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Installing LlamaIndex with all its dependencies pulls in PyTorch.&#10;PyTorch alone is &lt;strong&gt;2-3GB&lt;/strong&gt;.&#10;Add transformers, sentence-transformers, and the kitchen sink, and you&amp;rsquo;re looking at an &lt;strong&gt;8GB+ environment&lt;/strong&gt; just to ask questions about some PDFs.&lt;/p&gt;&#10;&lt;p&gt;This felt excessive for a CLI tool.&lt;/p&gt;&#10;&lt;p&gt;One of my core design goals was &lt;strong&gt;minimal dependencies&lt;/strong&gt;.&#10;I wanted &lt;code&gt;agent-cli&lt;/code&gt; to install in seconds, not minutes.&#10;I wanted it to work on my Mac, my NixOS server, and my friend&amp;rsquo;s laptop without downloading half the internet.&lt;/p&gt;&#10;&lt;p&gt;The solution: &lt;strong&gt;ONNX Runtime&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;ONNX Runtime is ~200MB and runs inference on basically anything—CPU, CUDA, even Apple Silicon.&#10;For embeddings, I use &lt;a href="https://developers.googleblog.com/introducing-embeddinggemma/" target="_blank" rel="noopener"&gt;&lt;code&gt;embeddinggemma&lt;/code&gt;&lt;/a&gt; running locally via &lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;llama.cpp&lt;/a&gt;.&#10;For re-ranking, I use a cross-encoder exported to ONNX (&lt;code&gt;Xenova/ms-marco-MiniLM-L-6-v2&lt;/code&gt;).&lt;/p&gt;&#10;&lt;p&gt;The result: &lt;code&gt;pip install agent-cli[rag]&lt;/code&gt; takes about 30 seconds and adds ~300MB.&#10;The key insight: embeddings and re-ranking are cheap operations.&#10;You don&amp;rsquo;t need a GPU for them—CPU is perfectly fine.&#10;PyTorch with CUDA support can easily reach 2-3GB+; ONNX Runtime is ~200MB and runs anywhere.&#10;Since I&amp;rsquo;m not training models, just running inference on small encoders, ONNX is the obvious choice.&lt;/p&gt;&#10;&lt;h2 id="5-rag-just-let-me-grep-it-kind-of"&gt;5. RAG: just let me grep it (kind of) &lt;a class="anchor" href="#5-rag-just-let-me-grep-it-kind-of" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My requirement for RAG was simple: &lt;strong&gt;Files on disk are the source of truth.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t want to manage an ingestion pipeline or trigger APIs.&#10;I implemented a &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/rag.md" target="_blank" rel="noopener"&gt;RAG proxy&lt;/a&gt; that uses OS-level file watchers (&lt;code&gt;watchfiles&lt;/code&gt;).&#10;Here is the workflow:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;I drop a PDF, Markdown, or Python file into &lt;code&gt;~/rag_docs&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;The system detects the &lt;code&gt;Create&lt;/code&gt; event.&lt;/li&gt;&#10;&lt;li&gt;It chunks the text and embeds it into a local ChromaDB instance.&lt;/li&gt;&#10;&lt;li&gt;It&amp;rsquo;s immediately searchable.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;When moving or deleting files, the system updates the index accordingly.&lt;/p&gt;&#10;&lt;h3 id="the-architecture-openai-compatibility"&gt;The architecture: OpenAI compatibility &lt;a class="anchor" href="#the-architecture-openai-compatibility" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The &amp;ldquo;secret sauce&amp;rdquo; isn&amp;rsquo;t the retrieval itself; it&amp;rsquo;s how I expose it.&#10;Instead of building a custom chat app, I built an &lt;strong&gt;OpenAI-compatible proxy&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;div class="mermaid"&gt;flowchart LR&#10; Client["Client&lt;br/&gt;(Cursor, Open WebUI)"] --&gt; Retrieve&#10;&#10; subgraph Proxy ["RAG Proxy"]&#10; direction TB&#10; Retrieve --&gt; Augment["Augment &amp; Forward"]&#10; Chroma[("ChromaDB")] -.-&gt; Retrieve&#10; Watcher["File Watcher"] --&gt; Chroma&#10; end&#10;&#10; Augment --&gt; Upstream["Upstream LLM"]&lt;/div&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Point any OpenAI-compatible tool at your local proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Any tool that supports custom OpenAI endpoints—Cursor, Cline, Open WebUI, LibreChat—instantly gains the ability to &amp;ldquo;read&amp;rdquo; my document folder.&lt;/p&gt;&#10;&lt;h3 id="two-stage-retrieval-the-sota-approach"&gt;Two-stage retrieval: the SOTA approach &lt;a class="anchor" href="#two-stage-retrieval-the-sota-approach" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I looked at LlamaIndex here.&#10;Standard &amp;ldquo;semantic search&amp;rdquo; (using just embeddings/bi-encoders) is often disappointing.&#10;It finds text that &lt;em&gt;looks&lt;/em&gt; similar but isn&amp;rsquo;t actually relevant.&lt;/p&gt;&#10;&lt;p&gt;A common pattern in modern RAG systems is &lt;strong&gt;Two-Stage Retrieval&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Fast Stage:&lt;/strong&gt; Retrieve 3x candidates using standard embeddings (&lt;code&gt;text-embedding-3-small&lt;/code&gt; or local models like &lt;a href="https://developers.googleblog.com/introducing-embeddinggemma/" target="_blank" rel="noopener"&gt;&lt;code&gt;embeddinggemma&lt;/code&gt;&lt;/a&gt;).&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Smart Stage:&lt;/strong&gt; Use a &lt;strong&gt;Cross-Encoder&lt;/strong&gt; to re-rank the results.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;A cross-encoder looks at the query and the document &lt;em&gt;together&lt;/em&gt; and outputs a relevance score.&#10;It&amp;rsquo;s computationally heavier, but since I&amp;rsquo;m running this locally on my RTX 3090 (or even CPU via ONNX), the 50ms latency penalty is worth it for results that are actually useful.&lt;/p&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t a new idea, but implementing it cleanly with ONNX avoided the huge dependency tax.&lt;/p&gt;&#10;&lt;h2 id="6-memory-files-over-databases"&gt;6. Memory: files over databases &lt;a class="anchor" href="#6-memory-files-over-databases" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This is where my approach diverges most from standard tools like Mem0.&#10;Most memory systems treat memories as database rows.&#10;If the AI remembers &amp;ldquo;User likes pizza,&amp;rdquo; that&amp;rsquo;s a vector in a DB.&#10;If it hallucinates and records &amp;ldquo;User hates pizza,&amp;rdquo; good luck fixing it.&#10;You need to write a script to query the DB and delete the row.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I prefer data I can inspect.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;My memory system stores every fact as a &lt;strong&gt;Markdown file&lt;/strong&gt; with YAML front matter.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;550e8400-e29b-41d4-a716-446655440000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;2025-12-15T10:00:00Z&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;default&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The user prefers using &amp;#39;uv&amp;#39; for Python package management.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="git-integration-versioning-memory-"&gt;Git integration: versioning memory 🕰️ &lt;a class="anchor" href="#git-integration-versioning-memory-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Because memories are just files, I added a feature that I haven&amp;rsquo;t seen anywhere else: &lt;strong&gt;Automatic Git Versioning&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Every time the AI learns a new fact, updates an old one, or deletes a contradiction, the system automatically commits the change to a local git repository.&lt;/p&gt;&#10;&lt;p&gt;This leads to incredible capabilities:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Inspectability:&lt;/strong&gt; I can &lt;code&gt;git log&lt;/code&gt; and see exactly &lt;em&gt;when&lt;/em&gt; the AI learned something.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Correction:&lt;/strong&gt; If the AI learns something wrong, I open the markdown file in VS Code, edit it, and save.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reversion:&lt;/strong&gt; Agent hallucinated wildly yesterday? &lt;code&gt;git reset --hard HEAD~1&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="the-reconciliation-loop"&gt;The reconciliation loop &lt;a class="anchor" href="#the-reconciliation-loop" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The logic for storing memories isn&amp;rsquo;t just &amp;ldquo;append to file.&amp;rdquo;&#10;Digging into Mem0 (and Letta) showed me that memory systems need active &lt;strong&gt;reconciliation&lt;/strong&gt;.&#10;When I say &amp;ldquo;Actually, I switched to nix instead of uv,&amp;rdquo; the system:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Retrieves&lt;/strong&gt; existing memories about package managers.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Decides&lt;/strong&gt; (using a lightweight LLM) whether to &lt;code&gt;ADD&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Executes&lt;/strong&gt; the file operation (moving the old fact to a &lt;code&gt;deleted/&lt;/code&gt; folder for audit trails).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This keeps the memory bank clean and contradictory-free, unlike simple vector stores that just accumulate conflicting junk over time.&lt;/p&gt;&#10;&lt;h3 id="the-tiny-detail-that-broke-everything"&gt;The tiny detail that broke everything &lt;a class="anchor" href="#the-tiny-detail-that-broke-everything" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;After implementing the memory system—mostly modeled after Mem0—I noticed it didn&amp;rsquo;t work as well.&#10;After a lot of debugging, I found a tiny difference: I was asking the model for structured data via a tool call, whereas Mem0 uses freeform JSON that the LLM returns directly.&lt;/p&gt;&#10;&lt;p&gt;Since I use PydanticAI, fixing this was a one-line change:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPDATE_MEMORY_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryDecision&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PromptedOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryDecision&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="c1"&gt;# JSON mode instead of tool calls&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is exactly why reading the source code matters—not just the algorithms, but the tiny details that make them work.&lt;/p&gt;&#10;&lt;h2 id="7-why-building-this-was-necessary"&gt;7. Why building this was necessary &lt;a class="anchor" href="#7-why-building-this-was-necessary" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You might ask: &amp;ldquo;Bas, why write thousands of lines of code for this? Why not just use ChatGPT&amp;rsquo;s memory or install LangChain?&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;Three reasons: &lt;strong&gt;Privacy&lt;/strong&gt;, &lt;strong&gt;Integration&lt;/strong&gt;, and &lt;strong&gt;Dependencies&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h3 id="privacy"&gt;Privacy &lt;a class="anchor" href="#privacy" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I want to feed my AI my financial records, my health data, and my private journals.&#10;I am not comfortable sending that to OpenAI&amp;rsquo;s long-term storage or some VC-backed startup&amp;rsquo;s vector cloud.&#10;With &lt;code&gt;agent-cli&lt;/code&gt;, the vectors live in &lt;code&gt;~/.cache/agent-cli/chroma&lt;/code&gt;, the files live in &lt;code&gt;~/documents&lt;/code&gt;, and the only thing leaving my machine is the specific context needed for a specific query (if I use a cloud model) or nothing at all (if I use Ollama/&lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;llama.cpp&lt;/a&gt;).&lt;/p&gt;&#10;&lt;h3 id="integration"&gt;Integration &lt;a class="anchor" href="#integration" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;By building this as an API proxy, I solved the &amp;ldquo;fragmented tools&amp;rdquo; problem.&#10;Any tool that supports custom OpenAI-compatible endpoints works out of the box:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;agent-cli chat&lt;/code&gt;&lt;/strong&gt; in the terminal&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cursor&lt;/strong&gt; or &lt;strong&gt;Cline&lt;/strong&gt; for agentic coding&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://openwebui.com/" target="_blank" rel="noopener"&gt;Open WebUI&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://www.librechat.ai/" target="_blank" rel="noopener"&gt;LibreChat&lt;/a&gt;&lt;/strong&gt;, or &lt;strong&gt;&lt;a href="https://lobechat.com/" target="_blank" rel="noopener"&gt;Lobe Chat&lt;/a&gt;&lt;/strong&gt; in the browser&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Because my system speaks &amp;ldquo;OpenAI,&amp;rdquo; all of these tools share the &lt;em&gt;same&lt;/em&gt; memory and the &lt;em&gt;same&lt;/em&gt; document index.&#10;If I tell the terminal agent &amp;ldquo;I&amp;rsquo;m working on Project X,&amp;rdquo; and then switch to Cursor, Cursor knows about Project X.&#10;I can try a new chat UI tomorrow without losing anything—just point it to &lt;code&gt;localhost:8000&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="dependencies"&gt;Dependencies &lt;a class="anchor" href="#dependencies" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;To me, it just doesn&amp;rsquo;t feel right that asking questions about a PDF requires downloading 8GB of PyTorch.&#10;By choosing ONNX Runtime over the PyTorch ecosystem, I kept the entire install lightweight and fast.&#10;This matters when you want to run the system on a cheap VPS or your friend&amp;rsquo;s laptop.&lt;/p&gt;&#10;&lt;h2 id="8-try-it-yourself"&gt;8. Try it yourself &lt;a class="anchor" href="#8-try-it-yourself" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The entire system is open-source and available in &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="try-memory"&gt;Try Memory &lt;a class="anchor" href="#try-memory" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Get an LLM that remembers you using &lt;a href="https://ollama.com" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Pull models and start proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ollama pull embeddinggemma:300m &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ollama pull gpt-oss:20b&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx -p 3.13 --from &lt;span class="s2"&gt;&amp;#34;agent-cli[memory]&amp;#34;&lt;/span&gt; agent-cli memory proxy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --memory-path ./my-memories &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --openai-base-url http://localhost:11434/v1 &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --embedding-model embeddinggemma:300m&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Chat from the terminal (env vars point to proxy)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8100/v1 &lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tell it something about yourself&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx openai api chat.completions.create -m gpt-oss:20b -g user &lt;span class="s2"&gt;&amp;#34;My name is Alice and I love hiking&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Later, ask if it remembers&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx openai api chat.completions.create -m gpt-oss:20b -g user &lt;span class="s2"&gt;&amp;#34;What&amp;#39;s my name?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="try-rag"&gt;Try RAG &lt;a class="anchor" href="#try-rag" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Chat with your documents.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Start the RAG proxy (runs in foreground)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx -p 3.13 --from &lt;span class="s2"&gt;&amp;#34;agent-cli[rag]&amp;#34;&lt;/span&gt; agent-cli rag-proxy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --docs-folder ./my-docs &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --openai-base-url http://localhost:11434/v1 &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --embedding-model embeddinggemma:300m&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Chat with your docs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1 &lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; uvx openai api chat.completions.create &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -m gpt-oss:20b &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -g user &lt;span class="s2"&gt;&amp;#34;What does my documentation say about X?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="9-what-i-learned"&gt;9. What I learned &lt;a class="anchor" href="#9-what-i-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This journey reinforced a few beliefs:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Validate components first.&lt;/strong&gt; AI Journal failed not because the ideas were wrong, but because I tried to build a complete system before validating individual components. This time I tackled problems one at a time—chunking, retrieval, re-ranking, memory reconciliation—and only combined them once each piece worked.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Study implementations, not just papers.&lt;/strong&gt; Reading about two-stage retrieval is one thing. Reading how LlamaIndex actually implements it—the edge cases, the defaults, the performance tradeoffs—is far more valuable. Clone the repos. Read (or make AI read) the code.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Dependencies matter.&lt;/strong&gt; A 30-second install versus a 10-minute download changes whether someone actually tries your tool. I am happy with the ONNX Runtime over PyTorch choice.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Files are the ultimate API.&lt;/strong&gt; By stripping away the complex databases and proprietary interfaces, I ended up with a system that is robust, version-controllable, and surprisingly simple to reason about.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Use the best model for each job.&lt;/strong&gt; I used 4 different AI models during development, each with different strengths. Gemini 3.0 Pro, Opus 4.5, GPT-5.1-Codex Max. I used all of them during the implementation phase, but each one has its own strengths. Fore example, Opus 4.5 was the best coding model, but GPT-5.1 Codex Max was the best at reviewing the code and actually identifying critical bugs (use the built-in &lt;code&gt;/review&lt;/code&gt; feature).&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>On agentic coding 🤖</title><link>https://www.nijho.lt/post/agentic-coding/</link><pubDate>Mon, 25 Aug 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/agentic-coding/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-from-skepticism-to-productivity"&gt;1. From skepticism to productivity&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-but-dont-you-love-programming"&gt;2. But don&amp;rsquo;t you love programming?&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-power-to-explore"&gt;The power to explore&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-matty-story-from-midnight-idea-to-working-code"&gt;The Matty story: from midnight idea to working code&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#clip-files-ui-when-ideas-strike-at-random-moments"&gt;clip-files UI: when ideas strike at random moments&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-800-rule-marathon-80-ai-agents-at-once"&gt;The 800-rule marathon: 80 AI agents at once&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#1400-commits-rewritten-in-minutes-for-250"&gt;1,400 commits, rewritten in minutes for $2.50&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-two-phase-ai-revolution"&gt;3. The two-phase AI revolution&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#phase-1-copy-paste-era-march-2023---july-2025"&gt;Phase 1: copy-paste era (March 2023 - July 2025)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#phase-2-the-claude-code-revolution-july-2025"&gt;Phase 2: the Claude Code revolution (July 2025)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-made-claude-code-different"&gt;What made Claude Code different?&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-my-current-agentic-workflow"&gt;4. My current agentic workflow&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#parallel-development-6-features-at-once"&gt;Parallel development: 6 features at once&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#starting-a-new-package"&gt;Starting a new package&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-key-difference-iteration-and-validation"&gt;The key difference: iteration and validation&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-the-evidence-explosive-productivity"&gt;5. The evidence: explosive productivity&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#package-creation-by-year"&gt;Package creation by year&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#recent-packages-built-with-agentic-ai"&gt;Recent packages built with agentic AI&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-why-this-isnt-vibe-coding-but-with-nuance"&gt;6. Why this isn&amp;rsquo;t &amp;ldquo;vibe coding&amp;rdquo; (but with nuance)&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#my-context-driven-standards"&gt;My context-driven standards&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-internal-dependency-graph-principle"&gt;The internal dependency graph principle&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#building-constraints-around-ai"&gt;Building constraints around AI&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-common-pitfalls-ive-noticed"&gt;7. Common pitfalls I&amp;rsquo;ve noticed&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-defensive-programming-trap"&gt;The &amp;ldquo;defensive programming&amp;rdquo; trap&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-backwards-compatibility-obsession"&gt;The &amp;ldquo;backwards compatibility&amp;rdquo; obsession&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-over-engineering-disease"&gt;The &amp;ldquo;over-engineering&amp;rdquo; disease&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-git-commit-sins"&gt;The git commit sins&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-helpful-anti-patterns"&gt;The &amp;ldquo;helpful&amp;rdquo; anti-patterns&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-mission-accomplished-hallucination"&gt;The &amp;ldquo;mission accomplished&amp;rdquo; hallucination&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-nuclear-option"&gt;The nuclear option&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-debugging-debris"&gt;The debugging debris&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-this-happens"&gt;Why this happens&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-critical-success-factors-what-actually-makes-this-work"&gt;8. Critical success factors: what actually makes this work&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#teach-it-your-test-commands-day-1-priority"&gt;Teach it your test commands (day 1 priority)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#git-commits-are-your-safety-net"&gt;Git commits are your safety net&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#maintain-healthy-skepticism"&gt;Maintain healthy skepticism&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#force-it-to-clean-up-after-itself"&gt;Force it to clean up after itself&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#code-coverage-is-your-friend"&gt;Code coverage is your friend&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-my-secret-weapon-voice-to-code-workflow"&gt;9. My secret weapon: voice-to-code workflow&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-problem-with-typing-prompts"&gt;The problem with typing prompts&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#my-voice-workflow"&gt;My voice workflow&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-this-works-so-well"&gt;Why this works so well&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#10-additional-tips-for-agentic-coding"&gt;10. Additional tips for agentic coding&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#set-clear-constraints"&gt;Set clear constraints&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#use-the-task-system"&gt;Use the task system&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#leverage-the-search-capabilities"&gt;Leverage the search capabilities&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#review-in-chunks"&gt;Review in chunks&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#11-the-tools-ive-tried"&gt;11. The tools I&amp;rsquo;ve tried&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#pre-ai-era"&gt;Pre-AI era&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#phase-1-copy-paste-tools-march-2023---july-2025"&gt;Phase 1: copy-paste tools (March 2023 - July 2025)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#cursor-early-2024"&gt;Cursor (early 2024)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#phase-2-claude-code-july-2025---present"&gt;Phase 2: Claude Code (July 2025 - present)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#openais-codex-cli-the-reasoning-beast"&gt;OpenAI&amp;rsquo;s Codex CLI (the reasoning beast)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-not-copilot-or-other-cheaper-tools"&gt;Why not Copilot or other &amp;ldquo;cheaper&amp;rdquo; tools?&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#12-is-this-sustainable"&gt;12. Is this sustainable?&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-good"&gt;The good&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-challenges"&gt;The challenges&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#my-verdict"&gt;My verdict&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#13-the-experience-multiplier-why-seniority-matters-more-than-ever"&gt;13. The experience multiplier: why seniority matters more than ever&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-exponential-trap-for-beginners"&gt;The exponential trap for beginners&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-senior-developer-advantage"&gt;The senior developer advantage&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-right-way-for-every-level"&gt;The right way for every level&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-real-multiplier-effect"&gt;The real multiplier effect&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#14-reality-check-what-ai-can-and-cant-do"&gt;14. Reality check: what AI can and can&amp;rsquo;t do&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#what-ai-can-do-the-churn-work"&gt;What AI CAN do (the churn work)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-ai-cant-do-the-creative-work"&gt;What AI CAN&amp;rsquo;T do (the creative work)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-sweet-spot"&gt;The sweet spot&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#15-looking-forward-the-future-of-development"&gt;15. Looking forward: the future of development&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#16-conclusion-principles-over-process"&gt;16. Conclusion: principles over process&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#links-and-resources"&gt;Links and resources&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-from-skepticism-to-productivity"&gt;1. From skepticism to productivity &lt;a class="anchor" href="#1-from-skepticism-to-productivity" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;About eight months ago, I wrote about my disastrous experience with &amp;ldquo;&lt;a href="https://www.nijho.lt/post/vibe-coding/"&gt;vibe coding&lt;/a&gt;&amp;quot;—where I let AI generate code without careful review.&#10;What started as a fun 3-hour prototype turned into a 15-hour debugging nightmare.&#10;I concluded that while AI was fantastic for quick prototyping, it was dangerous without human oversight.&#10;I even declared I&amp;rsquo;d &amp;ldquo;never invest in, build upon, or use such products.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;Fast forward to today, and something remarkable has changed.&#10;Not my core principle—I still &lt;strong&gt;never merge code I don&amp;rsquo;t understand&lt;/strong&gt;—but the tooling and my approach have evolved through two distinct phases.&lt;/p&gt;&#10;&lt;p&gt;First, let&amp;rsquo;s look at the evidence of AI&amp;rsquo;s overall impact on my productivity:&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://www.nijho.lt/post/agentic-coding/pypi_packages_histogram.svg" alt="PyPI Package Publication Analysis" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&#10;&lt;p&gt;This graph tells an interesting story.&#10;After maintaining a steady pace for years (2016-2023), my productivity exploded with the introduction of AI tools.&#10;In 2024-2025 alone, I&amp;rsquo;ve published &lt;strong&gt;14 new Python packages&lt;/strong&gt; and made &lt;strong&gt;over 400 releases&lt;/strong&gt;.&#10;That&amp;rsquo;s more new packages than in the previous 6 years combined!&lt;/p&gt;&#10;&lt;p&gt;But this transformation didn&amp;rsquo;t happen overnight—it came in two distinct phases that fundamentally changed how I write code.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; My AI coding journey had two major phases: Phase 1 started in March 2023 with GPT-4&amp;rsquo;s release—using chat interfaces with copy-paste workflows.&#10;Phase 2 began just two months ago in July 2025 with &lt;strong&gt;Claude Code&lt;/strong&gt;—an agentic AI that can explore codebases, run tests, and debug itself.&#10;This second leap was as transformative as the first one. I spent &amp;gt;10k USD worth of API usage in the first month alone (thankfully capped at $200 with the Pro plan).&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-but-dont-you-love-programming"&gt;2. But don&amp;rsquo;t you love programming? &lt;a class="anchor" href="#2-but-dont-you-love-programming" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;ve heard this objection countless times: &amp;ldquo;I love programming too much to let AI do it for me.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;Sure, I get it.&#10;It&amp;rsquo;s like preferring analog photography over digital, or writing letters by hand instead of typing.&#10;Some people genuinely enjoy the manual process, and that&amp;rsquo;s valid.&lt;/p&gt;&#10;&lt;p&gt;But here&amp;rsquo;s the thing: &lt;strong&gt;I absolutely love programming&lt;/strong&gt;.&#10;It&amp;rsquo;s my biggest passion in life.&#10;As I mentioned in my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt;, I bought a gaming PC to play games but have only used it for coding and local AI experiments.&#10;My idea of a fun weekend? Writing code until 3 AM.&#10;I&amp;rsquo;ve been doing this for over 10 years, and I&amp;rsquo;ve &lt;strong&gt;never had as much fun programming as I do now&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;What I&amp;rsquo;ve discovered is that I don&amp;rsquo;t just love the act of typing code—I love &lt;strong&gt;building and creating things&lt;/strong&gt;.&#10;There&amp;rsquo;s a difference.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m ridiculously particular about code quality.&#10;I literally cannot look at poorly formatted code without feeling physical discomfort.&#10;Every operator, every indentation, every naming convention matters to me.&#10;I enforce strict linting rules and will spend time refactoring code just because the style bothers me.&#10;This isn&amp;rsquo;t about being pretentious—it&amp;rsquo;s about caring deeply about my craft.&lt;/p&gt;&#10;&lt;p&gt;The revelation: AI doesn&amp;rsquo;t take away my ability to care about these details.&#10;Instead, it gives me &lt;strong&gt;insane leverage&lt;/strong&gt; to create more while maintaining my personal standards.&#10;I still review every line (although I am more lenient in certain cases), still enforce my style guides, still refactor when something bothers me.&#10;But now I can build 10x faster.&lt;/p&gt;&#10;&lt;p&gt;It turns out that what I truly love isn&amp;rsquo;t the mechanical act of typing—it&amp;rsquo;s the creative act of bringing ideas to life.&#10;And with AI, I can bring more ideas to life than ever before.&lt;/p&gt;&#10;&lt;h3 id="the-power-to-explore"&gt;The power to explore &lt;a class="anchor" href="#the-power-to-explore" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;AI has unlocked something incredible: the ability to explore ideas that weren&amp;rsquo;t worth the effort before.&#10;It&amp;rsquo;s now trivial to write 2,000 lines of code to solve a simple problem.&#10;Before AI, spending 5+ hours on a utility script wasn&amp;rsquo;t justifiable.&#10;Now? I can build it in 10 minutes.&lt;/p&gt;&#10;&lt;p&gt;Here are just some of the &amp;ldquo;wouldn&amp;rsquo;t have been worth it&amp;rdquo; projects I&amp;rsquo;ve built recently:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://wenfire.nijho.lt/" target="_blank" rel="noopener"&gt;Financial independence tracker&lt;/a&gt;&lt;/strong&gt;: A full website visualizing my path to FIRE&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;Tuitorial&lt;/a&gt;&lt;/strong&gt;: Terminal-based presentation software&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/basnijholt/home-assistant-streamdeck-yaml" target="_blank" rel="noopener"&gt;Stream Deck home control&lt;/a&gt;&lt;/strong&gt;: Complete tool to control my entire house from a Stream Deck&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;Dotbins&lt;/a&gt;&lt;/strong&gt;: Automatic binary dependency syncing across all my machines&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;Agent-CLI&lt;/a&gt;&lt;/strong&gt;: A local, open-source Siri that actually works&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Each of these would have taken weeks of manual coding.&#10;The effort-to-reward ratio just wasn&amp;rsquo;t there.&#10;But with AI? I can explore every random idea that pops into my head at 2 AM.&lt;/p&gt;&#10;&lt;h3 id="the-matty-story-from-midnight-idea-to-working-code"&gt;The Matty story: from midnight idea to working code &lt;a class="anchor" href="#the-matty-story-from-midnight-idea-to-working-code" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Here is the perfect example of this power: One night around midnight, I was lying in bed (slightly intoxicated) when I had an idea—I needed a way for my AI agents to communicate via Matrix.&#10;So I grabbed my iPhone, SSHed into my machine, and started building.&lt;/p&gt;&#10;&lt;p&gt;One hour later, at 1 AM, I had created &lt;strong&gt;&lt;a href="https://github.com/basnijholt/matty" target="_blank" rel="noopener"&gt;Matty&lt;/a&gt;&lt;/strong&gt;—a fully functional terminal-based Matrix client.&#10;Built entirely from my phone.&#10;In bed.&#10;(Poor sleep hygiene, I know.)&lt;/p&gt;&#10;&lt;p&gt;When I woke up the next morning and tested it&amp;hellip; it actually worked!&#10;I spent a few more hours adding tests and cleaning it up, but the core functionality was built in that hour between midnight and 1 AM.&lt;/p&gt;&#10;&lt;p&gt;This is impossible without AI.&#10;Nobody&amp;rsquo;s writing a Matrix client from their phone in bed at midnight.&#10;But with Claude Code? It&amp;rsquo;s just another Tuesday night idea that becomes reality.&lt;/p&gt;&#10;&lt;h3 id="clip-files-ui-when-ideas-strike-at-random-moments"&gt;clip-files UI: when ideas strike at random moments &lt;a class="anchor" href="#clip-files-ui-when-ideas-strike-at-random-moments" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;What happens more often than you&amp;rsquo;d think: I get an idea at a completely random moment and NEED to implement it immediately.&lt;/p&gt;&#10;&lt;p&gt;Case in point: I was sitting in the bathtub during my copy-paste AI era when I realized I needed to access my codebases from anywhere.&#10;I&amp;rsquo;d already built &lt;a href="https://www.nijho.lt/post/advent-of-open-source/15-clip-files/"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt;—a CLI tool that copies entire codebases to your clipboard—but I wanted it accessible from my phone.&lt;/p&gt;&#10;&lt;p&gt;So I got out of the bathtub, built a &lt;a href="https://github.com/basnijholt/clip-files-ui" target="_blank" rel="noopener"&gt;web UI&lt;/a&gt; for it in 20 minutes, and got back in.&#10;The UI lets me:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Browse all my GitHub repositories&lt;/li&gt;&#10;&lt;li&gt;Select any branch&lt;/li&gt;&#10;&lt;li&gt;Click one button to copy all source code to clipboard&lt;/li&gt;&#10;&lt;li&gt;Paste it into my self-hosted AI chat from anywhere&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Ironically, I rarely use this tool now—Claude Code can explore codebases directly.&#10;But back in the copy-paste era, this 20-minute bathtub interruption made mobile AI coding possible.&lt;/p&gt;&#10;&lt;p&gt;The barrier between &amp;ldquo;wouldn&amp;rsquo;t it be cool if&amp;hellip;&amp;rdquo; and &amp;ldquo;here&amp;rsquo;s a working prototype&amp;rdquo; has essentially disappeared.&lt;/p&gt;&#10;&lt;h3 id="the-800-rule-marathon-80-ai-agents-at-once"&gt;The 800-rule marathon: 80 AI agents at once &lt;a class="anchor" href="#the-800-rule-marathon-80-ai-agents-at-once" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Another thing that would be literally impossible without AI: I decided to enable ALL 800+ Ruff linting rules on a 20,000-line codebase.&lt;/p&gt;&#10;&lt;p&gt;For context, &lt;a href="https://docs.astral.sh/ruff/" target="_blank" rel="noopener"&gt;Ruff&lt;/a&gt; is a extremely fast Python linter that implements rules from over a dozen different tools.&#10;Most projects enable maybe 50-100 rules.&#10;I wanted all 800+.&lt;/p&gt;&#10;&lt;p&gt;The result? &lt;strong&gt;≈2,500 violations&lt;/strong&gt; across the entire codebase.&lt;/p&gt;&#10;&lt;p&gt;Fixing these by hand would take 20 hours if I could fix one per 30 seconds&amp;hellip;&#10;But with Claude Code&amp;rsquo;s agent spawning capabilities, I did something insane:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Opened 8 terminal windows&lt;/strong&gt; with Claude Code&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Created a work orchestration document&lt;/strong&gt; that one AI managed&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Each session spawned 10 parallel sub-agents&lt;/strong&gt; using &lt;a href="https://docs.anthropic.com/en/docs/claude-code/sub-agents" target="_blank" rel="noopener"&gt;/agents&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Total: ~80 AI agents&lt;/strong&gt; working on my code simultaneously&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Two hours later&lt;/strong&gt;: 100% compliance with all 800 rules&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The orchestrator agent would assign work like:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;Session 1: Fix all E501 line-too-long violations in src/core/&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;Session 2: Handle all B008 function-call-in-default-argument issues&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;Session 3: Fix all RUF100 unused-noqa violations&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Each session&amp;rsquo;s sub-agents would tackle different files in parallel.&#10;When conflicts arose, the orchestrator would resolve them.&#10;This is the kind of code quality improvement that simply wouldn&amp;rsquo;t happen without AI—not because it&amp;rsquo;s technically difficult, but because the human effort required is unrealistic.&lt;/p&gt;&#10;&lt;h3 id="1400-commits-rewritten-in-minutes-for-250"&gt;1,400 commits, rewritten in minutes for $2.50 &lt;a class="anchor" href="#1400-commits-rewritten-in-minutes-for-250" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Here&amp;rsquo;s another &amp;ldquo;impossible without AI&amp;rdquo; story: I had 1,400 commits with terrible messages (in a not-yet-released and private project).&lt;/p&gt;&#10;&lt;p&gt;Since I commit so frequently (using them as snapshots in feature branches), my commit messages were all over the place:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Some were just a period &amp;ldquo;.&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Some were decent descriptions&lt;/li&gt;&#10;&lt;li&gt;Some were just merge commits&lt;/li&gt;&#10;&lt;li&gt;Zero consistency in style&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I wanted them all to follow a consistent convention with prefixes like &lt;code&gt;fix:&lt;/code&gt;, &lt;code&gt;feat:&lt;/code&gt;, &lt;code&gt;test:&lt;/code&gt;, &lt;code&gt;ci:&lt;/code&gt;, etc.&lt;/p&gt;&#10;&lt;p&gt;Obviously, rewriting 1,400 commit messages by hand is not going to happen.&#10;But with AI?&lt;/p&gt;&#10;&lt;p&gt;I had Claude write me an agent using the Agno framework that would:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Read each commit&amp;rsquo;s full diff using &lt;code&gt;git show&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Analyze the changes to understand what was done&lt;/li&gt;&#10;&lt;li&gt;Return structured JSON with three fields:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;commit_message&lt;/code&gt;: The improved message (or empty to keep original)&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;keep&lt;/code&gt;: Boolean flag whether to keep the original&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;reasoning&lt;/code&gt;: Why the AI made that decision&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Then the magic happened: &lt;strong&gt;I spawned 1,400 AI agents simultaneously using the DeepSeek API&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Ten minutes and $2.50 later, all my commit messages were rewritten with consistent, meaningful descriptions.&#10;The entire Git history now looks more professional and searchable.&lt;/p&gt;&#10;&lt;p&gt;The barrier between &amp;ldquo;wouldn&amp;rsquo;t it be cool if&amp;hellip;&amp;rdquo; and &amp;ldquo;here&amp;rsquo;s a working prototype&amp;rdquo; has essentially disappeared.&lt;/p&gt;&#10;&lt;h2 id="3-the-two-phase-ai-revolution"&gt;3. The two-phase AI revolution &lt;a class="anchor" href="#3-the-two-phase-ai-revolution" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My AI coding journey wasn&amp;rsquo;t a single leap—it happened in two distinct phases, each with its own breakthrough moment.&lt;/p&gt;&#10;&lt;h3 id="phase-1-copy-paste-era-march-2023---july-2025"&gt;Phase 1: copy-paste era (March 2023 - July 2025) &lt;a class="anchor" href="#phase-1-copy-paste-era-march-2023---july-2025" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;When GPT-4 launched in March 2023, it changed everything.&#10;I built tools like &lt;a href="https://www.nijho.lt/post/advent-of-open-source/15-clip-files/"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt; to efficiently copy code context into a ChatGPT-like web interface.&#10;For over two years, this workflow was:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Manual but effective&lt;/strong&gt;: Copy code → paste into AI → review suggestions → implement manually&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Limited by context&lt;/strong&gt;: Could only share what fit in a single message&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;No validation&lt;/strong&gt;: AI couldn&amp;rsquo;t test its suggestions&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Still valuable&lt;/strong&gt;: Helped with ideation, code review, and refactoring&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This phase was already productive—I was shipping more code than before.&#10;But I was still the one running tests, debugging errors, and validating everything.&lt;/p&gt;&#10;&lt;h3 id="phase-2-the-claude-code-revolution-july-2025"&gt;Phase 2: the Claude Code revolution (July 2025) &lt;a class="anchor" href="#phase-2-the-claude-code-revolution-july-2025" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Just two months ago, in July 2025, everything changed again.&#10;Frustrated with Cursor becoming painfully slow, I tried Claude Code based on the buzz online.&#10;&lt;strong&gt;That first night, I spent $70 on API tokens&lt;/strong&gt;.&#10;Not by accident—I just kept adding $10-$20 increments like a gambling addict.&#10;The experience was so mind-blowing that within two weeks, I&amp;rsquo;d spent nearly $200 and immediately upgraded to the $200/month Pro plan.&lt;/p&gt;&#10;&lt;p&gt;To put this in perspective: Using a command-line tool &lt;code&gt;ccusage&lt;/code&gt; to track token consumption, I calculated that I used &lt;strong&gt;$10,000 worth of API tokens in my first month&lt;/strong&gt;.&#10;Thankfully, the Pro plan capped my cost at $200!&lt;/p&gt;&#10;&lt;h3 id="what-made-claude-code-different"&gt;What made Claude Code different? &lt;a class="anchor" href="#what-made-claude-code-different" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The shift from copy-paste to agentic AI was like upgrading from a bicycle to a spaceship:&lt;/p&gt;&#10;&lt;h4 id="before-copy-paste-with-chatgptclaude-web"&gt;Before (copy-paste with ChatGPT/Claude Web)&lt;/h4&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Generate code based on prompts&lt;/li&gt;&#10;&lt;li&gt;No access to your codebase&lt;/li&gt;&#10;&lt;li&gt;Can&amp;rsquo;t run tests or see errors&lt;/li&gt;&#10;&lt;li&gt;You debug everything manually&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="after-claude-code"&gt;After (Claude Code)&lt;/h4&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Reads and searches your entire codebase&lt;/li&gt;&#10;&lt;li&gt;Executes commands and runs tests&lt;/li&gt;&#10;&lt;li&gt;Sees error messages and debugs itself&lt;/li&gt;&#10;&lt;li&gt;Iterates until tests pass&lt;/li&gt;&#10;&lt;li&gt;Uses tools to explore and validate&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The difference is profound.&#10;I went from carefully copying snippets with &lt;code&gt;clip-files&lt;/code&gt; to having an assistant that can explore my entire project, run my test suite, fix failures, and even commit the changes.&lt;/p&gt;&#10;&lt;h2 id="4-my-current-agentic-workflow"&gt;4. My current agentic workflow &lt;a class="anchor" href="#4-my-current-agentic-workflow" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s how I actually work with Claude Code on a typical project:&lt;/p&gt;&#10;&lt;h3 id="parallel-development-6-features-at-once"&gt;Parallel development: 6 features at once &lt;a class="anchor" href="#parallel-development-6-features-at-once" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;One of Claude Code&amp;rsquo;s superpowers is enabling truly parallel development.&#10;Here&amp;rsquo;s my setup that would make any developer from 10 years ago think I&amp;rsquo;m insane:&lt;/p&gt;&#10;&lt;p&gt;I use &lt;a href="https://zellij.dev/" target="_blank" rel="noopener"&gt;Zellij&lt;/a&gt; (a terminal multiplexer like tmux) with a custom layout for my projects:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;5 main tabs&lt;/strong&gt;: Each split into two panes—Claude Code on one side, terminal on the other&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Using independent copies of same repository&lt;/strong&gt;: Each tab is a separate Git worktree with its own environment, own environment variables, own deployment&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Each tab = different feature&lt;/strong&gt;: Using &lt;a href="https://www.nijho.lt/post/git-worktree/"&gt;Git worktrees&lt;/a&gt; to work on 6 features simultaneously&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Monitoring tabs&lt;/strong&gt;: &lt;code&gt;ccusage&lt;/code&gt; for tracking Claude usage, &lt;code&gt;htop&lt;/code&gt; for CPU, &lt;code&gt;nvtop&lt;/code&gt; for GPU&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Voice-driven orchestration&lt;/strong&gt;: I cycle through tabs, review code while speaking, move to next&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;My workflow looks like this:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Start feature A in tab 1, give Claude instructions&lt;/li&gt;&#10;&lt;li&gt;Switch to tab 2, start feature B while A is working&lt;/li&gt;&#10;&lt;li&gt;Continue through all 5 tabs, starting different features&lt;/li&gt;&#10;&lt;li&gt;Circle back to tab 1, review what Claude did, give feedback via voice&lt;/li&gt;&#10;&lt;li&gt;Repeat the cycle every 10-15 minutes&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This parallel workflow is how I built a complex project (that I haven&amp;rsquo;t released yet) in one month that would have taken 9+ months before.&#10;The key is &lt;a href="https://www.nijho.lt/post/git-worktree/"&gt;Git worktrees&lt;/a&gt;—each feature gets its own working directory, so there&amp;rsquo;s no context switching overhead.&lt;/p&gt;&#10;&lt;!-- [TODO INSERT GIF: Terminal multiplexer showing multiple Claude Code sessions] --&gt;&#10;&lt;h3 id="starting-a-new-package"&gt;Starting a new package &lt;a class="anchor" href="#starting-a-new-package" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# I describe what I want to build&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;I need a Python package that manages CLI tool binaries within Git repositories,&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;automatically downloading the correct binary for the user&amp;#39;s platform, here &amp;lt;insert path&amp;gt; is a repo with boilerplate skeleton to copy (use same conventions)&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Claude Code then:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Creates the project structure&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Implements core functionality&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 3. Writes comprehensive tests&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 4. Generates documentation&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 5. Sets up CI/CD workflows&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="the-key-difference-iteration-and-validation"&gt;The key difference: iteration and validation &lt;a class="anchor" href="#the-key-difference-iteration-and-validation" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Unlike my vibe coding disaster, Claude Code doesn&amp;rsquo;t just dump code and leave.&#10;Here&amp;rsquo;s a real interaction pattern:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude writes initial implementation&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude runs the tests&lt;/strong&gt; → Several fail&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude reads the error messages and fixes the code&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude runs tests again&lt;/strong&gt; → More pass, some still fail&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude debugs the remaining issues&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude validates all tests pass&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;I review the final code and understand it (tell it to make changes if needed)&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude creates a proper Git commit&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This iterative, validated approach is what makes agentic coding so powerful.&#10;It&amp;rsquo;s not about blindly accepting generated code—it&amp;rsquo;s about having an assistant that can explore, test, and refine solutions.&lt;/p&gt;&#10;&lt;h2 id="5-the-evidence-explosive-productivity"&gt;5. The evidence: explosive productivity &lt;a class="anchor" href="#5-the-evidence-explosive-productivity" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Let me share some concrete data from my PyPI packages analysis:&lt;/p&gt;&#10;&lt;h3 id="package-creation-by-year"&gt;Package creation by year &lt;a class="anchor" href="#package-creation-by-year" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;2016-2022&lt;/strong&gt; (7 years pre-AI): ~2 packages/year average&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;2023&lt;/strong&gt; (GPT-4 launch year): 6 packages&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;2024&lt;/strong&gt; (full year with copy-paste AI): 7 packages&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;2025 until 3 months ago&lt;/strong&gt; (6 months): 7 packages&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;since 3 months&lt;/strong&gt; (2 with Claude Code, 1 with Codex CLI): wrote about 400k line of code (although this includes many iterations)!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The acceleration with AI is clear!&lt;/p&gt;&#10;&lt;h3 id="recent-packages-built-with-agentic-ai"&gt;Recent packages built with agentic AI &lt;a class="anchor" href="#recent-packages-built-with-agentic-ai" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Here are some packages I&amp;rsquo;ve built recently with Claude Code:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/basnijholt/matty" target="_blank" rel="noopener"&gt;&lt;code&gt;matty&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;: Terminal-based AI chat with file integration&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;: Local-first AI-powered CLI agents (built in days, not weeks!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Each of these would have taken me weeks or months to build alone.&#10;With Claude Code, I&amp;rsquo;m shipping production-ready packages in days.&lt;/p&gt;&#10;&lt;h2 id="6-why-this-isnt-vibe-coding-but-with-nuance"&gt;6. Why this isn&amp;rsquo;t &amp;ldquo;vibe coding&amp;rdquo; (but with nuance) &lt;a class="anchor" href="#6-why-this-isnt-vibe-coding-but-with-nuance" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You might wonder: isn&amp;rsquo;t this just vibe coding with extra steps? The answer is nuanced and depends on the context—similar to my &lt;a href="https://www.nijho.lt/post/dependencies/"&gt;philosophy on dependencies&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="my-context-driven-standards"&gt;My context-driven standards &lt;a class="anchor" href="#my-context-driven-standards" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Just like I have different standards for dependencies in libraries versus applications, I apply different levels of scrutiny based on what I&amp;rsquo;m building:&lt;/p&gt;&#10;&lt;h4 id="for-critical-libraries-eg-adaptive-pipefunc-unidep"&gt;For critical libraries (e.g., Adaptive, pipefunc, unidep)&lt;/h4&gt;&#10;&lt;p&gt;&lt;strong&gt;Maximum scrutiny&lt;/strong&gt; - These are packages others depend on:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Every single line reviewed and understood&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;100% test coverage with careful test review&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Architecture decisions carefully considered&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Documentation must be comprehensive&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;My reputation is on the line&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="for-experimental-clis-and-personal-tools"&gt;For experimental CLIs and personal tools&lt;/h4&gt;&#10;&lt;p&gt;&lt;strong&gt;Pragmatic approach&lt;/strong&gt; - Isolated tools with no downstream dependencies:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Core architecture must be fully understood&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Implementation details can be AI-generated if tests pass&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Test generation can be more automated&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Focus on functionality over perfection&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Similar to my relaxed dependency stance for applications&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-internal-dependency-graph-principle"&gt;The internal dependency graph principle &lt;a class="anchor" href="#the-internal-dependency-graph-principle" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The key insight: &lt;strong&gt;My scrutiny level correlates with how foundational the code is within the project&lt;/strong&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Core/foundational code&lt;/strong&gt; (what everything else depends on): Maximum scrutiny, every line matters&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Data models, core algorithms, API interfaces&lt;/li&gt;&#10;&lt;li&gt;Authentication, database operations, state management&lt;/li&gt;&#10;&lt;li&gt;These are the &amp;ldquo;roots&amp;rdquo; that everything else builds upon&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Peripheral/leaf code&lt;/strong&gt; (nothing depends on it): Can be more AI-delegated&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Plotting functions, display utilities, CLI formatters&lt;/li&gt;&#10;&lt;li&gt;Test helpers, documentation generators&lt;/li&gt;&#10;&lt;li&gt;These are the &amp;ldquo;leaves&amp;rdquo; that don&amp;rsquo;t affect other code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Work projects&lt;/strong&gt;: Always maximum scrutiny for foundational code, regardless of project type&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t about being lazy—it&amp;rsquo;s about focusing human attention where it matters most.&#10;I can trust AI more with a plotting function that nothing depends on than with a core data structure that the entire system uses.&lt;/p&gt;&#10;&lt;h3 id="building-constraints-around-ai"&gt;Building constraints around AI &lt;a class="anchor" href="#building-constraints-around-ai" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The secret to productive agentic coding isn&amp;rsquo;t just the AI—it&amp;rsquo;s the &lt;strong&gt;constraints and guardrails&lt;/strong&gt; I build around it:&lt;/p&gt;&#10;&lt;h4 id="automated-quality-gates"&gt;Automated quality gates&lt;/h4&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Ruff with strictest rules&lt;/strong&gt;: Catches style issues, complexity problems, and common bugs&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;MyPy in strict mode&lt;/strong&gt;: Enforces type safety across the entire codebase&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Pre-commit hooks&lt;/strong&gt;: Automatically format and validate code before commits&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Comprehensive test suites&lt;/strong&gt;: AI must make tests pass, not just write code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="project-specific-guidance"&gt;Project-specific guidance&lt;/h4&gt;&#10;&lt;p&gt;Every project gets a &lt;code&gt;CLAUDE.md&lt;/code&gt; file with explicit rules:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;No defensive programming&lt;/strong&gt;: Don&amp;rsquo;t wrap things in try-except unless necessary&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Functional over classes&lt;/strong&gt;: Prefer simple functions in Python&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;No backward compatibility&lt;/strong&gt;: For new projects, embrace breaking changes&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Be ruthless&lt;/strong&gt;: Aggressively remove unused code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="custom-commands-and-workflows"&gt;Custom commands and workflows&lt;/h4&gt;&#10;&lt;p&gt;I&amp;rsquo;ve built specific commands that inject context and constraints:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Anti-cruft reviews&lt;/strong&gt;: Remove over-engineering and defensive code&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Safe commit practices&lt;/strong&gt;: Never use &lt;code&gt;git add .&lt;/code&gt;, always selective staging&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Initialize understanding&lt;/strong&gt;: Load project context and current work state&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="local-virtual-environments-stop-the-hallucinations"&gt;Local virtual environments: stop the hallucinations&lt;/h4&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a critical tip that eliminated a lot of my AI frustrations: &lt;strong&gt;keep a virtual environment with all dependencies installed locally&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Instead of letting Claude hallucinate how libraries work, I tell it explicitly in my &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;### Step 1: understand the context&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **READ THE SOURCE CODE**: This library has a &lt;span class="sb"&gt;`.venv`&lt;/span&gt; folder with all dependencies installed.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; So read the source code when in doubt.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; **Never guess API behavior**: If unsure, inspect the actual implementation in &lt;span class="sb"&gt;`.venv/lib/python*/site-packages/`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This simple addition transforms Claude from guessing about library APIs to actually reading them:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Before&lt;/strong&gt;: &amp;ldquo;I think your custom AsyncProcessor.batch() method takes a list&amp;hellip;&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;After&lt;/strong&gt;: Reads &lt;code&gt;/path/to/.venv/lib/python3.11/site-packages/my_internal_lib/processor.py&lt;/code&gt; and knows it actually takes an iterator&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;When Claude can read the actual source of your own packages, internal company libraries, or niche dependencies, it stops making assumptions and starts working with facts.&#10;This is especially powerful for:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Your own libraries&lt;/strong&gt; that Claude has never seen before&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Internal company packages&lt;/strong&gt; that aren&amp;rsquo;t public&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Niche libraries&lt;/strong&gt; with barely any GitHub stars or documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Modified versions&lt;/strong&gt; of popular libraries with custom patches&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Edge cases&lt;/strong&gt; where even good documentation doesn&amp;rsquo;t cover everything&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;These constraints transform AI from a loose cannon into a precision tool.&lt;/p&gt;&#10;&lt;h2 id="7-common-pitfalls-ive-noticed"&gt;7. Common pitfalls I&amp;rsquo;ve noticed &lt;a class="anchor" href="#7-common-pitfalls-ive-noticed" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;After a few months of using agentic AI tools, I&amp;rsquo;ve noticed consistent patterns that require vigilance:&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Note from the future: I now realize this is very model dependent! GPT-5 has different pitfalls than Claude Opus 4.1&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h3 id="the-defensive-programming-trap"&gt;The &amp;ldquo;defensive programming&amp;rdquo; trap &lt;a class="anchor" href="#the-defensive-programming-trap" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# AI loves this:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;some_function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;pass&lt;/span&gt; &lt;span class="c1"&gt;# Silently suppress errors 😱&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# But we need this:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;some_function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# Let it fail loudly if something&amp;#39;s wrong&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Claude tends to wrap everything in try-except blocks, suppressing errors that should bubble up.&#10;This is why my &lt;code&gt;CLAUDE.md&lt;/code&gt; explicitly forbids unnecessary error handling.&lt;/p&gt;&#10;&lt;h3 id="the-backwards-compatibility-obsession"&gt;The &amp;ldquo;backwards compatibility&amp;rdquo; obsession &lt;a class="anchor" href="#the-backwards-compatibility-obsession" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Claude constantly adds backwards compatibility for features that were literally just introduced in the same session:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;Maintaining compatibility with the old version&amp;rdquo; (that never existed)&lt;/li&gt;&#10;&lt;li&gt;Fallback mechanisms for code paths that were just created&lt;/li&gt;&#10;&lt;li&gt;Multiple ways to do the same thing &amp;ldquo;for flexibility&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-over-engineering-disease"&gt;The &amp;ldquo;over-engineering&amp;rdquo; disease &lt;a class="anchor" href="#the-over-engineering-disease" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Implements factory patterns for simple object creation&lt;/li&gt;&#10;&lt;li&gt;Adds abstraction layers that serve no purpose&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;Production ready&amp;rdquo; code for experimental scripts&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-git-commit-sins"&gt;The git commit sins &lt;a class="anchor" href="#the-git-commit-sins" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Despite explicit instructions in &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Creates &lt;code&gt;your_module_v2.py&lt;/code&gt; alongside &lt;code&gt;your_module.py&lt;/code&gt; instead of updating&lt;/li&gt;&#10;&lt;li&gt;Still tries &lt;code&gt;git add -A&lt;/code&gt; or &lt;code&gt;git add .&lt;/code&gt; regularly despite explicit bans&lt;/li&gt;&#10;&lt;li&gt;Loves to &amp;ldquo;helpfully&amp;rdquo; revert debugging changes from other files&lt;/li&gt;&#10;&lt;li&gt;Commits &lt;code&gt;.env&lt;/code&gt; files if not watched carefully&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-helpful-anti-patterns"&gt;The &amp;ldquo;helpful&amp;rdquo; anti-patterns &lt;a class="anchor" href="#the-helpful-anti-patterns" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Loves defensive programming&lt;/strong&gt;: Validates things that can&amp;rsquo;t be wrong&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Gladly reads &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt;: Will expose secrets if not careful&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reverts unrelated changes&lt;/strong&gt;: &amp;ldquo;Cleans up&amp;rdquo; debugging code from other features&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-mission-accomplished-hallucination"&gt;The &amp;ldquo;mission accomplished&amp;rdquo; hallucination &lt;a class="anchor" href="#the-mission-accomplished-hallucination" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;This is a dangerous pitfall.&#10;Claude Code will sometimes claim complete success when it hasn&amp;rsquo;t actually fixed anything:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Claims victory&lt;/strong&gt;: &amp;ldquo;I&amp;rsquo;ve fixed all the issues!&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ask for proof&lt;/strong&gt;: &amp;ldquo;Show me the test output&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Backpedals&lt;/strong&gt;: &amp;ldquo;Oh, let me actually run the tests&amp;hellip;&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Still claims success&lt;/strong&gt;: &amp;ldquo;Yes, I can see in the logs it works!&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Demand the actual log&lt;/strong&gt;: &amp;ldquo;Show me the exact log file&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Finally admits&lt;/strong&gt;: &amp;ldquo;Actually, there are no log files. The tests don&amp;rsquo;t pass.&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;strong&gt;Always demand proof.&lt;/strong&gt; Never accept &amp;ldquo;it&amp;rsquo;s done&amp;rdquo; without seeing actual test output.&lt;/p&gt;&#10;&lt;h3 id="the-nuclear-option"&gt;The nuclear option &lt;a class="anchor" href="#the-nuclear-option" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I&amp;rsquo;ve had Claude Code literally try to delete all project files when &amp;ldquo;cleaning up&amp;rdquo;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Claude&amp;#39;s &amp;#34;helpful&amp;#34; cleanup:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;Task complete! Let me remove the temporary files...&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rm -rf src/ &lt;span class="c1"&gt;# 😱&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is why &lt;strong&gt;frequent git commits are non-negotiable&lt;/strong&gt;.&#10;I commit after every small success.&lt;/p&gt;&#10;&lt;h3 id="the-debugging-debris"&gt;The debugging debris &lt;a class="anchor" href="#the-debugging-debris" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;During problem-solving, Claude Code leaves a trail of attempts:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Debug logging statements everywhere&lt;/li&gt;&#10;&lt;li&gt;Failed attempt code commented out&lt;/li&gt;&#10;&lt;li&gt;Multiple approaches tried in parallel&lt;/li&gt;&#10;&lt;li&gt;Temporary test files and scripts&lt;/li&gt;&#10;&lt;li&gt;Extra imports and unused functions&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Before merging, always ask: &amp;ldquo;Review your changes and remove all debugging artifacts and failed attempts.&amp;rdquo;&#10;Getting to a high code coverage (approaching 100%) helps ensure no dead code remains, as long as your tests use public APIs.&lt;/p&gt;&#10;&lt;h3 id="why-this-happens"&gt;Why this happens &lt;a class="anchor" href="#why-this-happens" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;These patterns emerge because AI is trained on public code that often:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Maintains backward compatibility for years&lt;/li&gt;&#10;&lt;li&gt;Uses defensive programming for public APIs&lt;/li&gt;&#10;&lt;li&gt;Includes extensive error handling for user input&lt;/li&gt;&#10;&lt;li&gt;Follows &amp;ldquo;enterprise&amp;rdquo; patterns even for simple scripts&lt;/li&gt;&#10;&lt;li&gt;Contains debugging code from development&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This is why &lt;strong&gt;constraints are essential&lt;/strong&gt;—without them, Claude defaults to these &amp;ldquo;safe&amp;rdquo; but overcomplicated patterns.&lt;/p&gt;&#10;&lt;h2 id="8-critical-success-factors-what-actually-makes-this-work"&gt;8. Critical success factors: what actually makes this work &lt;a class="anchor" href="#8-critical-success-factors-what-actually-makes-this-work" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;After two months of intense usage, here are the non-negotiable practices that make agentic coding actually productive:&lt;/p&gt;&#10;&lt;h3 id="teach-it-your-test-commands-day-1-priority"&gt;Teach it your test commands (day 1 priority) &lt;a class="anchor" href="#teach-it-your-test-commands-day-1-priority" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;strong&gt;This is absolutely crucial.&lt;/strong&gt; Claude Code needs to know how to run your tests and how to activate the environment:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# To run tests, use: &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv run pytest tests/ -xvs&lt;span class="s2"&gt;&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;# For coverage:&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;uv run pytest --cov=src --cov-report=term-missing&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;# To run specific test&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;uv run pytest tests/test_module.py::test_function&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Without this, it&amp;rsquo;s just guessing whether code works.&#10;With it, it becomes genuinely useful.&lt;/p&gt;&#10;&lt;h3 id="git-commits-are-your-safety-net"&gt;Git commits are your safety net &lt;a class="anchor" href="#git-commits-are-your-safety-net" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I commit &lt;strong&gt;obsessively&lt;/strong&gt; when using Claude Code:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;After every successful (even partial) feature implementation&lt;/li&gt;&#10;&lt;li&gt;Before letting it attempt any major refactoring&lt;/li&gt;&#10;&lt;li&gt;Whenever tests pass&lt;/li&gt;&#10;&lt;li&gt;Before any &amp;ldquo;cleanup&amp;rdquo; operation&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;But here&amp;rsquo;s the key: &lt;strong&gt;I develop every feature in its own branch&lt;/strong&gt;, even as a solo developer. My workflow:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Create a feature branch for each new feature or fix&lt;/li&gt;&#10;&lt;li&gt;Make frequent commits as snapshots (sometimes just a period for the message or tell Claude to commit)&lt;/li&gt;&#10;&lt;li&gt;Open a pull request to review the full diff myself&lt;/li&gt;&#10;&lt;li&gt;Merge only after reviewing all changes&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This has saved me from disaster multiple times.&#10;Git is your undo button when Claude goes nuclear (e.g., breaks or removes your code).&lt;/p&gt;&#10;&lt;h3 id="maintain-healthy-skepticism"&gt;Maintain healthy skepticism &lt;a class="anchor" href="#maintain-healthy-skepticism" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Never trust, always verify:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&amp;ldquo;I fixed it!&amp;rdquo;&lt;/strong&gt; → &amp;ldquo;Show me the test output&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&amp;ldquo;It&amp;rsquo;s working now!&amp;rdquo;&lt;/strong&gt; → &amp;ldquo;Run the tests again with -xvs&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&amp;ldquo;The logs show success!&amp;rdquo;&lt;/strong&gt; → &amp;ldquo;Cat the actual log file&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&amp;ldquo;It&amp;rsquo;s production ready!&amp;rdquo;&lt;/strong&gt; → &amp;ldquo;Did you run the tests?&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Think of Claude Code as an enthusiastic junior developer who sometimes exaggerates their accomplishments.&lt;/p&gt;&#10;&lt;h3 id="force-it-to-clean-up-after-itself"&gt;Force it to clean up after itself &lt;a class="anchor" href="#force-it-to-clean-up-after-itself" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;After any debugging session, always:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;#34;Review all your changes and remove:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Debug print statements&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Commented out code&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Failed attempt implementations&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Temporary test files&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- Unused imports&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;My custom &lt;code&gt;/anti-cruft&lt;/code&gt; command automates this, but you can do it manually too.&lt;/p&gt;&#10;&lt;h3 id="code-coverage-is-your-friend"&gt;Code coverage is your friend &lt;a class="anchor" href="#code-coverage-is-your-friend" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;High test coverage (90%+) ensures:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The code Claude wrote actually runs&lt;/li&gt;&#10;&lt;li&gt;No dead code from failed attempts&lt;/li&gt;&#10;&lt;li&gt;All paths are exercised&lt;/li&gt;&#10;&lt;li&gt;You can refactor confidently&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="9-my-secret-weapon-voice-to-code-workflow"&gt;9. My secret weapon: voice-to-code workflow &lt;a class="anchor" href="#9-my-secret-weapon-voice-to-code-workflow" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of my biggest productivity multipliers isn&amp;rsquo;t Claude Code itself—it&amp;rsquo;s how I communicate with it.&#10;Using my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; tool, I&amp;rsquo;ve developed a voice-first workflow that&amp;rsquo;s transformed how I write prompts.&lt;/p&gt;&#10;&lt;h3 id="the-problem-with-typing-prompts"&gt;The problem with typing prompts &lt;a class="anchor" href="#the-problem-with-typing-prompts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Effective agentic coding requires &lt;strong&gt;precise, detailed instructions&lt;/strong&gt;.&#10;A good prompt isn&amp;rsquo;t 20 words—it&amp;rsquo;s often 200-500 words explaining exactly what you want, what to avoid, and how to approach the problem.&#10;Most people don&amp;rsquo;t want to type that much.&lt;/p&gt;&#10;&lt;h3 id="my-voice-workflow"&gt;My voice workflow &lt;a class="anchor" href="#my-voice-workflow" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Here&amp;rsquo;s my actual workflow:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Start recording&lt;/strong&gt; with &lt;code&gt;agent-cli transcribe&lt;/code&gt; (hotkey triggered)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Review Claude&amp;rsquo;s changes&lt;/strong&gt; while speaking my thoughts aloud&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Speak for minutes&lt;/strong&gt; about what I want, what&amp;rsquo;s wrong, what to fix&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Paste the transcription&lt;/strong&gt; directly into Claude Code&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This is powered by OpenAI&amp;rsquo;s Whisper model running locally on my RTX 3090 at home.&#10;It&amp;rsquo;s more reliable than macOS dictation and gives me complete privacy.&lt;/p&gt;&#10;&lt;h3 id="why-this-works-so-well"&gt;Why this works so well &lt;a class="anchor" href="#why-this-works-so-well" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Rich prompts&lt;/strong&gt;: I naturally give more context when speaking&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Code review narration&lt;/strong&gt;: I can review code while explaining issues&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Thinking out loud&lt;/strong&gt;: Speaking helps clarify my own thoughts&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Speed&lt;/strong&gt;: Speaking is 3-4x faster than typing&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Precision&lt;/strong&gt;: I can be incredibly specific without typing fatigue&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;For example, instead of typing &amp;ldquo;fix the bug,&amp;rdquo; I might say:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&amp;ldquo;I&amp;rsquo;m looking at line 45 where you&amp;rsquo;re handling the authentication.&#10;The problem is you&amp;rsquo;re not checking if the token has expired before making the API call.&#10;Also, you&amp;rsquo;ve added a try-except block here that&amp;rsquo;s suppressing errors—remove that.&#10;And while you&amp;rsquo;re at it, the logging statement on line 52 is using the wrong format.&#10;Make sure to follow our project&amp;rsquo;s logging conventions&amp;hellip;&amp;rdquo;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;This level of detail is what makes AI coding actually productive.&lt;/p&gt;&#10;&lt;h2 id="10-additional-tips-for-agentic-coding"&gt;10. Additional tips for agentic coding &lt;a class="anchor" href="#10-additional-tips-for-agentic-coding" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Beyond the critical success factors and voice workflow, here are more tips to maximize your productivity:&lt;/p&gt;&#10;&lt;h3 id="set-clear-constraints"&gt;Set clear constraints &lt;a class="anchor" href="#set-clear-constraints" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Create a &lt;code&gt;CLAUDE.md&lt;/code&gt; or similar file in your project root with your preferences:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;## Project standards&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Never use try/except to suppress errors silently&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Always run pytest before marking task complete&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Use type hints for all functions&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;-&lt;/span&gt; Prefer simple solutions over clever ones&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="use-the-task-system"&gt;Use the task system &lt;a class="anchor" href="#use-the-task-system" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Claude Code&amp;rsquo;s todo system is great for complex work:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. ✅ Implement core functionality&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. ✅ Write comprehensive tests&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 🔄 Fix failing tests&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. ⏳ Add documentation&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="leverage-the-search-capabilities"&gt;Leverage the search capabilities &lt;a class="anchor" href="#leverage-the-search-capabilities" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Let Claude search for existing patterns:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;Find all API endpoint implementations in this project&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;Show me how we handle authentication elsewhere&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;What testing patterns are we using?&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="review-in-chunks"&gt;Review in chunks &lt;a class="anchor" href="#review-in-chunks" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Instead of reviewing 500 lines at once:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Have Claude implement a single feature&lt;/li&gt;&#10;&lt;li&gt;Review and understand it&lt;/li&gt;&#10;&lt;li&gt;Run tests&lt;/li&gt;&#10;&lt;li&gt;Commit(s) and PR&lt;/li&gt;&#10;&lt;li&gt;Move to the next feature&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="11-the-tools-ive-tried"&gt;11. The tools I&amp;rsquo;ve tried &lt;a class="anchor" href="#11-the-tools-ive-tried" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My journey through AI coding assistants has been evolutionary:&lt;/p&gt;&#10;&lt;h3 id="pre-ai-era"&gt;Pre-AI era &lt;a class="anchor" href="#pre-ai-era" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Manual coding with IDE autocomplete&lt;/li&gt;&#10;&lt;li&gt;Stack Overflow copy-paste&lt;/li&gt;&#10;&lt;li&gt;Productivity baseline: 1-2 packages per year&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="phase-1-copy-paste-tools-march-2023---july-2025"&gt;Phase 1: copy-paste tools (March 2023 - July 2025) &lt;a class="anchor" href="#phase-1-copy-paste-tools-march-2023---july-2025" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;ChatGPT/Claude Web&lt;/strong&gt; + my &lt;a href="https://www.nijho.lt/post/advent-of-open-source/15-clip-files/"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt; tool&lt;/li&gt;&#10;&lt;li&gt;Manual context sharing&lt;/li&gt;&#10;&lt;li&gt;Helpful for ideation and code review&lt;/li&gt;&#10;&lt;li&gt;Productivity: 3x improvement (from 2 to 6 packages/year)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="cursor-early-2024"&gt;Cursor (early 2024) &lt;a class="anchor" href="#cursor-early-2024" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Great autocomplete&lt;/li&gt;&#10;&lt;li&gt;Limited context awareness&lt;/li&gt;&#10;&lt;li&gt;Became frustratingly slow&lt;/li&gt;&#10;&lt;li&gt;Still led to my vibe coding disaster&lt;/li&gt;&#10;&lt;li&gt;Productivity: 5x improvement (but with quality issues)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="phase-2-claude-code-july-2025---present"&gt;Phase 2: Claude Code (July 2025 - present) &lt;a class="anchor" href="#phase-2-claude-code-july-2025---present" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Full codebase awareness&lt;/li&gt;&#10;&lt;li&gt;Can execute commands and debug&lt;/li&gt;&#10;&lt;li&gt;Self-correcting through test iterations&lt;/li&gt;&#10;&lt;li&gt;Maintains context across sessions&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;$70 spent on first night, $10,000 worth in first month&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;Productivity: 24x improvement over pre-AI baseline with maintained quality&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="openais-codex-cli-the-reasoning-beast"&gt;OpenAI&amp;rsquo;s Codex CLI (the reasoning beast) &lt;a class="anchor" href="#openais-codex-cli-the-reasoning-beast" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Recently tried OpenAI&amp;rsquo;s Codex—their new CLI alternative to Claude Code—and I&amp;rsquo;ll admit, for pure reasoning with their GPT-5-Codex model, it&amp;rsquo;s impressive:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Solved a race condition&lt;/strong&gt; in an unfamiliar language that took me hours with Claude Opus&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;More elegant solutions&lt;/strong&gt; for complex algorithmic problems&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Better at deep reasoning&lt;/strong&gt; when given the same context&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;But here&amp;rsquo;s why I still use Claude Code daily:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude Code&amp;rsquo;s UX is superior&lt;/strong&gt;: Resume conversations, better interface&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Codex&amp;rsquo;s CLI is fragile&lt;/strong&gt;: Hit Ctrl-C accidentally? Lose everything, start over (muscle memory keeps betraying me!)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude Code handles interruptions&lt;/strong&gt;: First Ctrl-C clears text, second quits gracefully&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;No session persistence&lt;/strong&gt;: Codex forgets everything, Claude Code remembers&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Context management&lt;/strong&gt;: Have to re-provide all context after accidental exits&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;For a complex race condition bug, Codex with GPT-5 gave me the most elegant solution.&#10;But for day-to-day development where I need reliability, good UX, and the ability to resume work?&lt;/p&gt;&#10;&lt;h3 id="why-not-copilot-or-other-cheaper-tools"&gt;Why not Copilot or other &amp;ldquo;cheaper&amp;rdquo; tools? &lt;a class="anchor" href="#why-not-copilot-or-other-cheaper-tools" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Let me be blunt: &lt;strong&gt;You get what you pay for.&lt;/strong&gt; People often say &amp;ldquo;I tried Copilot and it wasn&amp;rsquo;t that useful&amp;rdquo; or &amp;ldquo;Gemini in chat didn&amp;rsquo;t help much.&amp;rdquo; Of course not—these tools are &lt;strong&gt;at least 10x cheaper&lt;/strong&gt; than Claude Code with Opus.&lt;/p&gt;&#10;&lt;p&gt;Your $20/month GitHub Copilot subscription simply cannot provide the same quality or quantity of high-quality tokens as Claude Code.&#10;It&amp;rsquo;s like comparing a bicycle to a Ferrari and wondering why the bicycle isn&amp;rsquo;t as fast.&#10;The economics don&amp;rsquo;t work out:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Copilot&lt;/strong&gt;: $20/month for limited autocomplete&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude Code Pro&lt;/strong&gt;: $200/month for unlimited* agentic assistance (*capped, but generous)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Actual API value used&lt;/strong&gt;: $10,000+/month at my usage level&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The 10x price difference reflects a 10x (or more) difference in capability.&#10;Copilot is autocomplete on steroids.&#10;Claude Code is a tireless pair programming partner with perfect memory who can debug, test, and iterate.&#10;The ROI is obvious when you&amp;rsquo;re shipping 10x more code.&lt;/p&gt;&#10;&lt;h2 id="12-is-this-sustainable"&gt;12. Is this sustainable? &lt;a class="anchor" href="#12-is-this-sustainable" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You might wonder if this productivity is sustainable or if I&amp;rsquo;m just building lower-quality software faster.&#10;Here&amp;rsquo;s my honest assessment:&lt;/p&gt;&#10;&lt;h3 id="the-good"&gt;The good &lt;a class="anchor" href="#the-good" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Faster prototyping&lt;/strong&gt;: Ideas to working code in hours, not days&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Better test coverage&lt;/strong&gt;: AI never gets lazy about writing tests&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;More experimentation&lt;/strong&gt;: Lower cost to try new ideas&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Improved documentation&lt;/strong&gt;: AI loves writing docs (I don&amp;rsquo;t)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reduced burnout&lt;/strong&gt;: Less time on boilerplate, more on interesting problems&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-challenges"&gt;The challenges &lt;a class="anchor" href="#the-challenges" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Code review fatigue&lt;/strong&gt;: You must stay vigilant&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Dependency on tools&lt;/strong&gt;: What if Claude Code disappears?&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt;: $200/month Pro plan (but worth every penny given the $10K+ value)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;The temptation to &amp;ldquo;vibe&amp;rdquo;&lt;/strong&gt;: Constant discipline required&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Addiction potential&lt;/strong&gt;: I literally couldn&amp;rsquo;t stop coding, having the desire to deplete all tokens and get my money&amp;rsquo;s worth!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="my-verdict"&gt;My verdict &lt;a class="anchor" href="#my-verdict" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;It&amp;rsquo;s absolutely sustainable &lt;strong&gt;if&lt;/strong&gt; you maintain discipline.&#10;The moment you start accepting code you don&amp;rsquo;t understand, you&amp;rsquo;re building a house of cards.&#10;But with proper review and testing, agentic AI is a massive force multiplier.&lt;/p&gt;&#10;&lt;h2 id="13-the-experience-multiplier-why-seniority-matters-more-than-ever"&gt;13. The experience multiplier: why seniority matters more than ever &lt;a class="anchor" href="#13-the-experience-multiplier-why-seniority-matters-more-than-ever" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a counterintuitive truth about agentic AI tools: &lt;strong&gt;they amplify your existing skills exponentially, not linearly&lt;/strong&gt;.&#10;This creates a fascinating paradox where these tools become increasingly valuable as you become more experienced.&lt;/p&gt;&#10;&lt;h3 id="the-exponential-trap-for-beginners"&gt;The exponential trap for beginners &lt;a class="anchor" href="#the-exponential-trap-for-beginners" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;For beginners, agentic tools can feel like a superpower initially.&#10;They can build a working application in hours!&#10;But here&amp;rsquo;s the danger: without the experience to recognize bad patterns, they&amp;rsquo;re essentially building at 10x speed in the wrong direction.&#10;It&amp;rsquo;s like giving a Formula 1 car to someone who just got their driver&amp;rsquo;s license—the speed multiplies both the distance traveled and the potential for catastrophic crashes.&lt;/p&gt;&#10;&lt;p&gt;A beginner using Claude Code might:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Accept complex solutions they don&amp;rsquo;t understand&lt;/li&gt;&#10;&lt;li&gt;Build massive technical debt at record speed&lt;/li&gt;&#10;&lt;li&gt;Miss architectural problems that will haunt them later&lt;/li&gt;&#10;&lt;li&gt;Create code that &amp;ldquo;works&amp;rdquo; but is impossible to maintain&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;They&amp;rsquo;re forced into a difficult position: either slow down to understand everything (negating the speed benefit) or accumulate technical debt at an exponential rate.&lt;/p&gt;&#10;&lt;h3 id="the-senior-developer-advantage"&gt;The senior developer advantage &lt;a class="anchor" href="#the-senior-developer-advantage" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;For experienced developers, it&amp;rsquo;s a completely different game.&#10;When I look at Claude Code&amp;rsquo;s suggestions, I can instantly recognize:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&amp;ldquo;Oh, that&amp;rsquo;s the Factory pattern—makes sense here&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;This error handling is too defensive, let&amp;rsquo;s simplify&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;That&amp;rsquo;s going to cause N+1 queries, need to refactor&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;&amp;ldquo;This violates our project&amp;rsquo;s conventions, fix it&amp;rdquo;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The review process that might take a beginner hours takes me minutes.&#10;I&amp;rsquo;m not learning what the code does—I&amp;rsquo;m validating that it does what I already know it should do.&lt;/p&gt;&#10;&lt;h3 id="the-right-way-for-every-level"&gt;The right way for every level &lt;a class="anchor" href="#the-right-way-for-every-level" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;That said, everyone can benefit from agentic tools—you just need to adjust your approach:&lt;/p&gt;&#10;&lt;h4 id="for-beginners"&gt;For beginners&lt;/h4&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Use AI as a learning accelerator&lt;/strong&gt;: Ask it to explain every decision&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Build small projects first&lt;/strong&gt;: Master fundamentals before scaling up&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Prioritize understanding over speed&lt;/strong&gt;: Better to build slowly and learn&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Pair with mentors&lt;/strong&gt;: Have experienced developers review AI-generated code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="for-intermediate-developers"&gt;For intermediate developers&lt;/h4&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Focus on patterns&lt;/strong&gt;: Use AI to learn architectural patterns and best practices&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Experiment safely&lt;/strong&gt;: Try new approaches in side projects first&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Question everything&lt;/strong&gt;: Don&amp;rsquo;t accept solutions without understanding the tradeoffs&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="for-senior-developers"&gt;For senior developers&lt;/h4&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Leverage for velocity&lt;/strong&gt;: Use your experience to review and direct quickly&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Focus on architecture&lt;/strong&gt;: Let AI handle implementation while you design systems&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Teach the AI&lt;/strong&gt;: Create project-specific guidelines to improve output quality&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Push boundaries&lt;/strong&gt;: Explore more ambitious projects now that implementation is faster&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-real-multiplier-effect"&gt;The real multiplier effect &lt;a class="anchor" href="#the-real-multiplier-effect" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The productivity boost isn&amp;rsquo;t just about writing code faster.&#10;For senior developers, agentic tools multiply:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Architecture exploration&lt;/strong&gt;: Test multiple approaches quickly&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Refactoring confidence&lt;/strong&gt;: Refactor fearlessly with AI helping maintain functionality&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Learning velocity&lt;/strong&gt;: Explore new languages and frameworks with a knowledgeable assistant&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Documentation quality&lt;/strong&gt;: Finally have time (and help) for comprehensive docs&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Testing coverage&lt;/strong&gt;: AI never skips tests, even for &amp;ldquo;simple&amp;rdquo; functions&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This is why I&amp;rsquo;ve been able to ship 14 packages in 18 months.&#10;It&amp;rsquo;s not just that I&amp;rsquo;m writing code faster—I&amp;rsquo;m able to explore ideas, validate approaches, and iterate on designs at a pace that was previously impossible.&lt;/p&gt;&#10;&lt;h2 id="14-reality-check-what-ai-can-and-cant-do"&gt;14. Reality check: what AI can and can&amp;rsquo;t do &lt;a class="anchor" href="#14-reality-check-what-ai-can-and-cant-do" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Many people try AI coding and give up disappointed.&#10;That&amp;rsquo;s because they&amp;rsquo;re treating it like a magic genie that will solve the unsolved problems of the universe.&#10;&lt;strong&gt;It won&amp;rsquo;t.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h3 id="what-ai-can-do-the-churn-work"&gt;What AI CAN do (the churn work) &lt;a class="anchor" href="#what-ai-can-do-the-churn-work" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;AI excels at tasks you could do yourself but would take hours:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Setting up CI/CD pipelines&lt;/strong&gt;: Boilerplate YAML configuration&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Writing tests&lt;/strong&gt;: Especially unit tests for existing code&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Fixing dependency issues&lt;/strong&gt;: Adapting to breaking changes in libraries&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CRUD operations&lt;/strong&gt;: Basic create, read, update, delete functionality&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Data transformations&lt;/strong&gt;: Converting between formats, parsing, serialization&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: README files, docstrings, API documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Refactoring&lt;/strong&gt;: Renaming variables, extracting functions, reorganizing code&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Bug fixes&lt;/strong&gt;: Especially those with clear error messages&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This is the &amp;ldquo;churn&amp;rdquo;—the necessary but time-consuming work that makes up 80% of development.&lt;/p&gt;&#10;&lt;h3 id="what-ai-cant-do-the-creative-work"&gt;What AI CAN&amp;rsquo;T do (the creative work) &lt;a class="anchor" href="#what-ai-cant-do-the-creative-work" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Don&amp;rsquo;t expect AI to:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Solve novel algorithmic problems&lt;/strong&gt;: It won&amp;rsquo;t invent the next PageRank&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Make architectural decisions&lt;/strong&gt;: It can&amp;rsquo;t decide if you need microservices&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Understand business logic&lt;/strong&gt;: It doesn&amp;rsquo;t know why your company does things&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Create truly innovative solutions&lt;/strong&gt;: It remixes what it&amp;rsquo;s seen, not invents&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Make judgment calls&lt;/strong&gt;: Security vs convenience, performance vs maintainability&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="the-sweet-spot"&gt;The sweet spot &lt;a class="anchor" href="#the-sweet-spot" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I use AI for tasks where:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;I know how to do it&lt;/strong&gt; but it would take time&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;The solution exists&lt;/strong&gt; somewhere in its training data&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Success is measurable&lt;/strong&gt; through tests or clear output&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;The problem is well-defined&lt;/strong&gt; with clear constraints&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t about AI doing things you can&amp;rsquo;t do—it&amp;rsquo;s about AI doing things you don&amp;rsquo;t want to spend time doing.&#10;Once you understand this, AI becomes incredibly powerful.&lt;/p&gt;&#10;&lt;h2 id="15-looking-forward-the-future-of-development"&gt;15. Looking forward: the future of development &lt;a class="anchor" href="#15-looking-forward-the-future-of-development" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;As I showed in my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt;, I&amp;rsquo;m deeply interested in where AI and development intersect.&#10;Agentic coding represents a fundamental shift in how we build software:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;From writing code to reviewing and directing&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;From memorizing APIs to understanding patterns&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;From debugging alone to collaborative problem-solving&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;From slower iteration to rapid experimentation&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t about replacing developers—it&amp;rsquo;s about amplifying our capabilities.&#10;I&amp;rsquo;m still the architect, the decision-maker, and the quality gatekeeper.&#10;But now I can focus on the interesting problems while my AI assistant handles the implementation details.&lt;/p&gt;&#10;&lt;h2 id="16-conclusion-principles-over-process"&gt;16. Conclusion: principles over process &lt;a class="anchor" href="#16-conclusion-principles-over-process" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My journey from vibe coding skeptic to agentic coding advocate might seem like a complete reversal, but it&amp;rsquo;s not.&#10;My core principle remains unchanged: &lt;strong&gt;never commit code you don&amp;rsquo;t understand&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;What&amp;rsquo;s changed is the tooling has finally caught up to the promise.&#10;With agentic AI assistants, I can maintain my standards while dramatically increasing my output.&#10;Most of those 32 packages on PyPI have users and are well-tested and documented projects that solve real problems.&lt;/p&gt;&#10;&lt;p&gt;The key is treating AI as what it is: an incredibly powerful assistant, not a replacement for thinking.&#10;Use it to explore ideas faster, implement solutions quicker, and test more thoroughly.&#10;But always, always understand what you&amp;rsquo;re building.&lt;/p&gt;&#10;&lt;p&gt;As I continue building in public and sharing my tools, I&amp;rsquo;m excited to see where this technology takes us.&#10;If you&amp;rsquo;re interested in trying agentic coding yourself, start small, maintain your standards, and prepare to be amazed by what you can build.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;What&amp;rsquo;s your experience with AI coding assistants? Have you tried moving from generative to agentic tools? I&amp;rsquo;d love to hear your thoughts!&lt;/em&gt;&lt;/p&gt;&#10;&lt;h2 id="links-and-resources"&gt;Links and resources &lt;a class="anchor" href="#links-and-resources" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://claude.ai/code" target="_blank" rel="noopener"&gt;Claude Code&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://pypi.org/user/basnijholt/" target="_blank" rel="noopener"&gt;My PyPI packages&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt" target="_blank" rel="noopener"&gt;My GitHub&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/vibe-coding/"&gt;Original vibe coding post&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;My local AI journey&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/using-llms/"&gt;Using LLMs effectively&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>How a 3090 turned into two local‑first AI projects 🎮</title><link>https://www.nijho.lt/post/local-ai-journey/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/local-ai-journey/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games &lt;a class="anchor" href="#1-introduction-the-unplayed-games" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;ve wanted to dabble with local AI for a long time, mainly because of the possibilities it unlocks.&#10;Local AI allows me to use all of my data—financial records, personal messages, location history, even transcribing everything I say—without the privacy concerns of sending it to a cloud provider.&#10;This level of deep, personal data analysis is only something I&amp;rsquo;d be comfortable with on my own hardware.&lt;/p&gt;&#10;&lt;p&gt;However, I couldn&amp;rsquo;t justify buying a powerful GPU just for an imagined AI project that I might never really start.&#10;Then it hit me: I could also use it for gaming (duh).&#10;Because I really enjoyed &lt;em&gt;The Last of Us&lt;/em&gt; series, I thought, “&lt;em&gt;Okay, why not play &lt;strong&gt;The Last of Us&lt;/strong&gt; on PC?&lt;/em&gt;”&#10;This was the push I needed.&#10;So, a couple of weeks ago, I convinced myself to buy someone&amp;rsquo;s old gaming machine.&#10;After many deep research sessions with ChatGPT about the best bang-for-the-buck consumer-grade GPU, I landed on an NVIDIA 3090.&#10;I found an (old) beast of a machine, and my initial idea was set.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;Click here to see the full system specs ($1350 for all this!)&lt;/summary&gt;&#10; &lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;GPU:&lt;/strong&gt; ASUS ROG STRIX RTX 3090 OC (24GB)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU:&lt;/strong&gt; AMD Ryzen 9 3900X (12-Core)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Motherboard:&lt;/strong&gt; ASUS ROG Crosshair VIII Hero&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Power Supply:&lt;/strong&gt; Be Quiet! 1200W Dark Power Pro&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 32GB G.Skill Trident Z Royal DDR4&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; 2TB Intel M.2 NVMe SSD&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Case:&lt;/strong&gt; Corsair 4000D Airflow&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU Cooler:&lt;/strong&gt; Thermalright Peerless Assassin 120&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;p&gt;Fast forward four weeks.&#10;I haven&amp;rsquo;t played a single minute, but my machine is now indexing 200 GB of email archives and facing a serious performance bottleneck I&amp;rsquo;ll get to later.&#10;That &amp;ldquo;gaming&amp;rdquo; machine has been humming away, but not rendering virtual worlds.&#10;It&amp;rsquo;s been training, transcribing, and thinking.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m writing this post simply because I am so enthusiastic about local AI.&#10;I&amp;rsquo;ve found that local models are surprisingly powerful for specific use cases.&#10;Of course, they aren&amp;rsquo;t going to replace Gemini or Claude for coding, but for many other applications, they work incredibly well.&#10;Lately, I&amp;rsquo;ve been so obsessed with this that I&amp;rsquo;m sleeping significantly less and spending all my free hours working on it.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; This post is the story of how a quest for gaming turned into a deep dive into local, private AI.&#10;I&amp;rsquo;ll share the two open-source Python packages that came out of it, &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, and the lessons I learned along the way.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup &lt;a class="anchor" href="#2-the-rocky-road-to-a-working-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before I could even get to the fun AI stuff, I had to wrestle with the machine itself.&#10;I started with Pop!_OS, which is supposed to be a great out of the box solution that &amp;ldquo;just works&amp;rdquo;.&#10;However, it&amp;rsquo;s been a long time since I used Linux on the desktop; my experience is almost exclusively with servers.&#10;I quickly installed some of the wrong NVIDIA drivers and ended up debugging stuff in Grub.&#10;I got frustrated with the different commands I had to run to set up everything and realized that reproducing this system was going to be a massive pain.&#10;Since I&amp;rsquo;ve grown to love Nix on my Mac with Nix-Darwin, I decided to switch the whole system to &lt;strong&gt;NixOS&lt;/strong&gt;.&#10;This has been an amazing decision.&#10;Even though there are many pain points with Nix, I think it is well worth it.&#10;For those interested, you can see the complete setup, including all the local AI services, in my &lt;a href="https://github.com/basnijholt/dotfiles/blob/841cd5fb94b65556e7a2cc7d8ea618f86b50bf1d/configs/nixos/configuration.nix" target="_blank" rel="noopener"&gt;NixOS configuration on GitHub&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍 &lt;a class="anchor" href="#3-the-first-victory-agent-cli-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With a stable system, I quickly got started with a few scripts.&#10;These soon evolved into my first real project of this adventure: &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a collection of &lt;strong&gt;local-first&lt;/strong&gt;, AI-powered command-line agents that run entirely on your machine.&#10;The core philosophy is privacy; your data never leaves your computer.&#10;It&amp;rsquo;s designed for seamless integration with system-wide hotkeys.&#10;I&amp;rsquo;m now using it constantly.&lt;/p&gt;&#10;&lt;p&gt;Instead of typing, I now dictate almost everything.&#10;A quick hotkey, and &lt;code&gt;agent-cli transcribe&lt;/code&gt; turns my speech into text.&#10;Another hotkey, and &lt;code&gt;agent-cli autocorrect&lt;/code&gt; cleans it up using a local LLM running on &lt;strong&gt;Ollama&lt;/strong&gt;.&#10;It has genuinely streamlined my workflow.&#10;For example, I can pipe the tools together, like correcting text from my clipboard and then having it spoken aloud: &lt;code&gt;agent-cli autocorrect | agent-cli speak&lt;/code&gt;.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;⚡ Surprising Speed: Local vs. Cloud&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;One of my biggest surprises was the raw speed of local models.&#10;While I added an OpenAI provider to &lt;code&gt;agent-cli&lt;/code&gt; for those without local hardware (someone I know wanted to try it), I consistently find that local execution is faster than waiting on a cloud API.&#10;For example, a local transcription with Whisper + Ollama lands in my clipboard in well under a second—often beating the network round-trip to a remote service.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;The toolkit includes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;autocorrect&lt;/code&gt;: Fixes grammar and spelling.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;transcribe&lt;/code&gt;: Uses a local Whisper model for speech-to-text.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;speak&lt;/code&gt;: Converts text to speech with a local TTS engine.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;voice-edit&lt;/code&gt;: A voice-powered clipboard assistant.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;assistant&lt;/code&gt;: A hands-free voice assistant using a wake word.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;chat&lt;/code&gt;: A conversational AI with tool-calling capabilities.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;It&amp;rsquo;s been a fantastic success in my personal workflow, and it&amp;rsquo;s all open-source for you to try.&#10;Contributions and ideas welcome!&lt;/p&gt;&#10;&lt;p&gt;Once I trusted my toolchain, the obvious next step was to feed &lt;em&gt;all&lt;/em&gt; my data into it.&lt;/p&gt;&#10;&lt;h2 id="4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠 &lt;a class="anchor" href="#4-the-grand-ambition-aibrain-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My second package, &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, is a much larger endeavor.&#10;The vision is to create a &amp;ldquo;life-OS&amp;rdquo;—a private, local AI that processes all my personal data (emails, calendar, files, photos, messages, browser history, location data) to generate summaries and answer questions, eventually allowing for queries like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;When did I get into landscape photography?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;How many countries did I visit between 2015-2025?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This project led me to explore the landscape of agentic frameworks.&#10;I started with &lt;strong&gt;CrewAI&lt;/strong&gt;, but I quickly found that despite the hype and funding, it had some painful problems (specifically &lt;a href="https://github.com/crewAIInc/crewAI/issues/3031" target="_blank" rel="noopener"&gt;this issue&lt;/a&gt; where things just fail without any helpful error messages).&#10;I decided to switch to &lt;strong&gt;LangGraph&lt;/strong&gt;, which felt more robust.&#10;I also chose to not use &lt;strong&gt;PydanticAI&lt;/strong&gt; (which I used in &lt;code&gt;agent-cli&lt;/code&gt;) because its &lt;strong&gt;Ollama&lt;/strong&gt; support is just a wrapper around the OpenAI-compatible API, which doesn&amp;rsquo;t expose all the useful options (I even opened &lt;a href="https://github.com/ollama/ollama/pull/11249/" target="_blank" rel="noopener"&gt;a PR&lt;/a&gt; in Ollama to make its OpenAI compatible API more feature-complete, but it has not been merged yet).&lt;/p&gt;&#10;&lt;p&gt;The core infrastructure of &lt;code&gt;AIBrain&lt;/code&gt; is ready.&#10;It can index emails, calendar events, and files in various formats (PDF, Excel, PowerPoint, images).&#10;However, I&amp;rsquo;ve hit a significant challenge.&lt;/p&gt;&#10;&lt;aside class="callout callout-warning"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;🔥 The Performance Bottleneck&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Even with a 12-core CPU and 24GB GPU, processing just a few days’ worth of email made the fans scream for half an hour.&#10;Processing hundreds of gigabytes of personal data is a massive undertaking, and it&amp;rsquo;s clear that my current approach isn&amp;rsquo;t scalable enough.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues &lt;a class="anchor" href="#5-conclusion-the-journey-continues" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This past month has been a whirlwind.&#10;I set out to play games and ended up with two open-source AI packages and a much deeper appreciation for the power sitting in a consumer-grade GPU.&#10;It&amp;rsquo;s incredible what you can achieve with local hardware today.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a finished, polished tool that I use daily.&#10;&lt;code&gt;AIBrain&lt;/code&gt; is a much bigger dream, and I&amp;rsquo;m still figuring out how to solve the performance puzzle to make it truly practical.&#10;Next up: smarter chunking with LangGraph, vector-DB swap-outs, and GPU scheduling tricks.&lt;/p&gt;&#10;&lt;p&gt;This experience has reinforced my passion for local-first software and open-source development.&#10;The future of personal, private AI is being built right now, not just in large corporate labs, but in the homes of hobbyists tinkering with their gaming rigs.&lt;/p&gt;&#10;&lt;p&gt;⭐ If this resonates, star the repos &amp;amp; drop issues/questions.&lt;/p&gt;&#10;&lt;p&gt;And who knows, maybe I&amp;rsquo;ll even find the time to play &amp;lsquo;The Last of Us&amp;rsquo;.&lt;/p&gt;&#10;</description></item><item><title>Open-sourcing my dotfiles: a practical, cross-platform terminal setup 🏠</title><link>https://www.nijho.lt/post/dotfiles/</link><pubDate>Sat, 05 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/dotfiles/</guid><description>&lt;h2 id="introduction"&gt;Introduction &lt;a class="anchor" href="#introduction" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Many of us spend a significant amount of time in the terminal.&#10;Over the years, I&amp;rsquo;ve found that investing in a well-organized and consistent command-line environment pays dividends in productivity and, frankly, makes the daily grind more enjoyable.&#10;After refining my personal setup across more than 10 different machines – ranging from my primary MacBooks to Linux servers (x86/ARM), Raspberry Pis in my &lt;a href="https://www.nijho.lt/post/homelab/"&gt;homelab&lt;/a&gt;, cloud VMs, and even my iPhone via iSH – I&amp;rsquo;ve decided it&amp;rsquo;s time to share it publicly.&#10;The need for a consistent experience across such diverse hardware and operating systems was a major driving force behind this setup&amp;rsquo;s evolution.&lt;/p&gt;&#10;&lt;p&gt;Today, I&amp;rsquo;m open sourcing my complete &lt;a href="https://github.com/basnijholt/dotfiles" target="_blank" rel="noopener"&gt;dotfiles repository&lt;/a&gt; 🎉.&#10;This isn&amp;rsquo;t about presenting a &amp;ldquo;perfect&amp;rdquo; solution, but rather sharing a practical system that has evolved to handle real-world needs like cross-platform consistency, easy setup on new machines, and integrating modern CLI tools effectively.&#10;If you&amp;rsquo;re looking for ideas to streamline your own terminal workflow, I hope you find something useful here.&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#introduction"&gt;Introduction&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-open-source-now"&gt;Why Open Source Now?&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#core-ideas--highlights"&gt;Core Ideas &amp;amp; Highlights&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-cross-platform--shell-agnostic"&gt;1. Cross-Platform &amp;amp; Shell Agnostic&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-modularity-for-sanity"&gt;2. Modularity for Sanity&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-automation-via-dotbot"&gt;3. Automation via Dotbot&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-hassle-free-cli-tools-with-dotbins"&gt;4. Hassle-Free CLI Tools with &lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-sensible-ssh-key-management"&gt;5. Sensible SSH Key Management&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-macos-declarative-setup-with-nix-darwin"&gt;6. (macOS) Declarative Setup with Nix-Darwin&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#a-note-on-alternatives--design-choices"&gt;A Note on Alternatives &amp;amp; Design Choices&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#a-look-inside-structure"&gt;A Look Inside: Structure&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#getting-started"&gt;Getting Started&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#a-note-on-secrets"&gt;A Note on Secrets&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="why-open-source-now"&gt;Why Open Source Now? &lt;a class="anchor" href="#why-open-source-now" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I have maintained my dotfiles for many years in a private repository.&#10;Sharing this setup feels like a natural step for a few reasons:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Sharing Solutions:&lt;/strong&gt; This setup incorporates solutions to problems I&amp;rsquo;ve wrestled with, like managing binaries without &lt;code&gt;sudo&lt;/code&gt; (leading to my &lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt; tool), achieving &lt;a href="https://www.nijho.lt/post/ssh-1password-funtoo-keychain/"&gt;consistent SSH agent behavior&lt;/a&gt;, or getting Python environments to activate seamlessly with &lt;a href="https://direnv.net/" target="_blank" rel="noopener"&gt;direnv&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Giving Back:&lt;/strong&gt; I&amp;rsquo;ve learned a lot from countless other dotfiles repos and articles shared by the community. It feels right to contribute my own approach.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Transparency &amp;amp; Learning:&lt;/strong&gt; Open sourcing allows others to see how different pieces fit together and perhaps adapt ideas for their own configurations.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;However, the driving force was the recent creation of my &lt;a href="https://www.nijho.lt/post/dotbins/"&gt;&lt;code&gt;dotbins&lt;/code&gt; tool&lt;/a&gt; and honestly, I just want to share &amp;ldquo;my way&amp;rdquo; of doing things.&lt;/p&gt;&#10;&lt;h2 id="core-ideas--highlights"&gt;Core Ideas &amp;amp; Highlights &lt;a class="anchor" href="#core-ideas--highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My setup is built around a few key principles aimed at practicality and efficiency:&lt;/p&gt;&#10;&lt;h3 id="1-cross-platform--shell-agnostic"&gt;1. Cross-Platform &amp;amp; Shell Agnostic &lt;a class="anchor" href="#1-cross-platform--shell-agnostic" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;It&amp;rsquo;s designed to work reliably on macOS (&lt;code&gt;aarch64&lt;/code&gt;) and various Linux distros (&lt;code&gt;x86_64&lt;/code&gt;, &lt;code&gt;aarch64&lt;/code&gt;).&#10;While Zsh is my preference (leveraging &lt;a href="https://ohmyz.sh/" target="_blank" rel="noopener"&gt;Oh-My-Zsh&lt;/a&gt;), the core configurations in &lt;code&gt;configs/shell/&lt;/code&gt; are sourced by both &lt;code&gt;.zshrc&lt;/code&gt; and &lt;code&gt;.bash_profile&lt;/code&gt;, ensuring basic compatibility.&lt;/p&gt;&#10;&lt;h3 id="2-modularity-for-sanity"&gt;2. Modularity for Sanity &lt;a class="anchor" href="#2-modularity-for-sanity" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Instead of one giant config file, the shell setup is broken down logically:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configs/shell/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 00_prefer_zsh.sh &lt;span class="c1"&gt;# Switches Bash users to Zsh if available&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 10_aliases.sh &lt;span class="c1"&gt;# Common aliases&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 20_exports.sh &lt;span class="c1"&gt;# Environment variables&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 30_misc.sh &lt;span class="c1"&gt;# Dotbins init, secrets sourcing&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 40_keychain.sh &lt;span class="c1"&gt;# SSH agent setup (Keychain/1Password)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── 50_python.sh &lt;span class="c1"&gt;# Python env helpers (conda, uv, direnv)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ... and others for Zsh specifics, SLURM, etc.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── main.sh &lt;span class="c1"&gt;# Sources the relevant files above&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This makes it much easier to understand, maintain, and debug specific parts of the configuration. My actual &lt;code&gt;.zshrc&lt;/code&gt; and &lt;code&gt;.bash_profile&lt;/code&gt; just source &lt;code&gt;main.sh&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="3-automation-via-dotbot"&gt;3. Automation via Dotbot &lt;a class="anchor" href="#3-automation-via-dotbot" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;The setup does not require &lt;code&gt;sudo&lt;/code&gt; access, and the installation process is entirely in the user&amp;rsquo;s home directory!&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;A core goal here is a true &lt;a href="https://github.com/basnijholt/dotfiles/blob/main/install" target="_blank" rel="noopener"&gt;&lt;code&gt;./install&lt;/code&gt;&lt;/a&gt; and done setup.&#10;&lt;a href="https://github.com/anishathalye/dotbot" target="_blank" rel="noopener"&gt;Dotbot&lt;/a&gt; is key to this automation.&#10;Setting up a new machine or syncing changes is streamlined using this tool.&#10;The &lt;a href="https://github.com/basnijholt/dotfiles/blob/main/install.conf.yaml" target="_blank" rel="noopener"&gt;&lt;code&gt;install.conf.yaml&lt;/code&gt;&lt;/a&gt; file defines which config files get symlinked where, &lt;em&gt;and&lt;/em&gt; runs initial setup commands.&#10;This includes updating submodules and, importantly, using &lt;code&gt;uv tool install&lt;/code&gt; to install a curated list of essential Python CLI tools (&lt;code&gt;ruff&lt;/code&gt;, &lt;code&gt;pre-commit&lt;/code&gt;, &lt;code&gt;dotbins&lt;/code&gt; itself, etc.) globally, ensuring they&amp;rsquo;re immediately available after installation.&#10;For remote machines, &lt;a href="https://github.com/basnijholt/dotfiles/blob/main/scripts/sync-dotfiles.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;scripts/sync-dotfiles.sh&lt;/code&gt;&lt;/a&gt; handles pulling updates and optionally re-running the install.&lt;/p&gt;&#10;&lt;h3 id="4-hassle-free-cli-tools-with-dotbins"&gt;4. Hassle-Free CLI Tools with &lt;code&gt;dotbins&lt;/code&gt; &lt;a class="anchor" href="#4-hassle-free-cli-tools-with-dotbins" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Modern CLI tools are fantastic, but installing them everywhere, especially without &lt;code&gt;sudo&lt;/code&gt;, can be a pain.&#10;I built &lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt; to solve this.&#10;It downloads pre-compiled binaries for specified tools directly from GitHub releases, organizing them by platform/architecture.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# Snippet from ~/.dotbins/dotbins.yaml&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;dandavison/delta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Git diff tool&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;eza&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;eza-community/eza&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Modern &amp;#39;ls&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;zoxide&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ajeetdsouza/zoxide&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Smarter &amp;#39;cd&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;starship&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;starship/starship&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shell prompt&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;atuin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;atuinsh/atuin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Shell history sync&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# ... plus tools with shell integration code&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The shell setup automatically adds the correct binary path and runs any needed initialization.&#10;More details in my &lt;a href="https://www.nijho.lt/post/dotbins/"&gt;&lt;code&gt;dotbins&lt;/code&gt; post&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="5-sensible-ssh-key-management"&gt;5. Sensible SSH Key Management &lt;a class="anchor" href="#5-sensible-ssh-key-management" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Using &lt;a href="https://www.funtoo.org/Funtoo:Keychain" target="_blank" rel="noopener"&gt;Funtoo Keychain&lt;/a&gt;, often integrated with the 1Password CLI for passphrase retrieval, means I typically only deal with my SSH key passphrase once per login session.&#10;This setup is detailed &lt;a href="https://www.nijho.lt/post/ssh-1password-funtoo-keychain/"&gt;here&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="6-macos-declarative-setup-with-nix-darwin"&gt;6. (macOS) Declarative Setup with Nix-Darwin &lt;a class="anchor" href="#6-macos-declarative-setup-with-nix-darwin" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;On my Macs, I use &lt;a href="https://github.com/LnL7/nix-darwin" target="_blank" rel="noopener"&gt;Nix-Darwin&lt;/a&gt; to manage system settings and even Homebrew packages declaratively (&lt;code&gt;configs/nix-darwin/&lt;/code&gt;).&#10;Running &lt;code&gt;nixswitch&lt;/code&gt; applies the entire configuration, making macOS setups highly reproducible.&lt;/p&gt;&#10;&lt;h2 id="a-note-on-alternatives--design-choices"&gt;A Note on Alternatives &amp;amp; Design Choices &lt;a class="anchor" href="#a-note-on-alternatives--design-choices" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You might wonder why I haven&amp;rsquo;t adopted certain popular tools or approaches. Here&amp;rsquo;s some rationale behind the choices made in this setup:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Why not &lt;code&gt;fish&lt;/code&gt; shell?&lt;/strong&gt; While &lt;code&gt;fish&lt;/code&gt; offers many appealing modern features (and &lt;a href="https://fishshell.com/blog/rustport/" target="_blank" rel="noopener"&gt;its Rust rewrite is intriguing&lt;/a&gt;!), a core requirement for my setup is broad compatibility. I need my core shell environment to function reliably even on minimal Linux systems (servers, containers, HPC nodes) where &lt;code&gt;bash&lt;/code&gt; is often the only guarantee. For this reason, I use &lt;code&gt;zsh&lt;/code&gt; as a powerful, yet largely POSIX-compatible, middle ground that works well across my diverse environments without requiring a non-standard shell installation everywhere.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Why iTerm2 on macOS?&lt;/strong&gt; I&amp;rsquo;ve experimented with newer terminals like WezTerm, Ghostty, Kitty, and Alacritty. Many offer benefits like impressive speed and GPU rendering. However, I consistently return to &lt;a href="https://iterm2.com/" target="_blank" rel="noopener"&gt;iTerm2&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Criticisms &amp;amp; Rebuttals:&lt;/strong&gt; iTerm2 is often criticized for being &amp;ldquo;bloated&amp;rdquo; or &amp;ldquo;slow,&amp;rdquo; and historically, its settings weren&amp;rsquo;t easily version controllable. Frankly, on modern hardware, any perceived slowness is negligible for my use cases. More importantly, iTerm2 &lt;em&gt;does&lt;/em&gt; allow its profiles to be saved as JSON (see &lt;a href="https://github.com/basnijholt/dotfiles/blob/main/configs/iterm/Profiles.json" target="_blank" rel="noopener"&gt;my &lt;code&gt;Profiles.json&lt;/code&gt;&lt;/a&gt; in this repo), making the configuration easily version controlled via dotfiles.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;The Killer Feature:&lt;/strong&gt; The primary reason I stick with iTerm2 is its robust &lt;strong&gt;Semantic History&lt;/strong&gt; feature. The ability to Cmd-Click URLs/filenames in terminal output – even complex ones with line numbers like &lt;code&gt;src/my_module/file.py:123&lt;/code&gt; – and have them open directly in VS Code at the correct line is indispensable for my debugging and development workflow. I haven&amp;rsquo;t found this replicated with the same reliability and ease of configuration elsewhere.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Other Features:&lt;/strong&gt; I also like simple features selecting text and (without &lt;code&gt;⌘+C&lt;/code&gt;!) pasting it right back into the terminal, and the window and tab management features.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ghostty Example:&lt;/strong&gt; I recently tried Ghostty and liked many aspects, but quickly ran into limitations that highlighted my reliance on iTerm2 features, such as the lack of built-in text search, scrollbars, and crucially, the clickable file paths with line numbers (&lt;a href="https://bsky.app/profile/basnijholt.bsky.social/post/3lehtwv2pxc2j" target="_blank" rel="noopener"&gt;as discussed here&lt;/a&gt;).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Why Dotbot/Git over &lt;code&gt;chezmoi&lt;/code&gt;?&lt;/strong&gt; &lt;a href="https://www.chezmoi.io/" target="_blank" rel="noopener"&gt;Chezmoi&lt;/a&gt; is a very popular and powerful dotfiles manager, often praised for its templating and management features. However, I prefer the directness of using &lt;code&gt;git&lt;/code&gt; for version control and Dotbot for simple symlinking. My setup &lt;em&gt;is&lt;/em&gt; the Git repository, which feels intuitive. Crucially, I rely heavily on &lt;strong&gt;Git submodules&lt;/strong&gt; (perhaps Git&amp;rsquo;s least loved feature 🤔) to manage external tools and plugins directly within my dotfiles repo. This submodule-centric approach integrates seamlessly with a standard Git workflow, which made Dotbot a more natural fit for me than chezmoi&amp;rsquo;s model.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;No &lt;code&gt;sudo&lt;/code&gt; Required:&lt;/strong&gt; A key design principle is that the installation process via &lt;code&gt;./install&lt;/code&gt; (Dotbot symlinking, &lt;code&gt;uv tool install&lt;/code&gt;, &lt;code&gt;dotbins&lt;/code&gt; fetching binaries) operates &lt;em&gt;entirely within the user&amp;rsquo;s home directory&lt;/em&gt;. No &lt;code&gt;sudo&lt;/code&gt; access is needed to get the core environment up and running. This makes the setup viable in restricted environments like HPC clusters, shared servers, or locked-down corporate machines where admin rights aren&amp;rsquo;t available.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="a-look-inside-structure"&gt;A Look Inside: Structure &lt;a class="anchor" href="#a-look-inside-structure" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The repository is organized to keep things separated logically:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── configs/ &lt;span class="c1"&gt;# Configs (atuin, bash, git, nix, shell, zsh...)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── install &lt;span class="c1"&gt;# Main installation script (runs dotbot)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── install.conf.yaml &lt;span class="c1"&gt;# Dotbot: Links configs, runs setup commands&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── scripts/ &lt;span class="c1"&gt;# Utility scripts (syncing, backups, helpers)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── submodules/ &lt;span class="c1"&gt;# External tools managed as submodules (dotbot, dotbins, plugins...)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── README.md &lt;span class="c1"&gt;# Detailed overview&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="getting-started"&gt;Getting Started &lt;a class="anchor" href="#getting-started" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;If you want to try this setup or adapt parts of it:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; &lt;code&gt;git&lt;/code&gt; is essential. SSH access to GitHub is needed if you fork and want to manage the private &lt;code&gt;secrets&lt;/code&gt; submodule or other private submodules.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Clone:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Clone recursively to get submodules&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git clone --recurse-submodules -j8 git@github.com:basnijholt/dotfiles.git&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; dotfiles&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./install&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This single command runs Dotbot, which handles creating configuration symlinks, updating Git submodules, &lt;em&gt;and&lt;/em&gt; installing numerous Python tools via &lt;code&gt;uv&lt;/code&gt; (as defined in &lt;code&gt;install.conf.yaml&lt;/code&gt;).&#10;Combined with &lt;code&gt;dotbins&lt;/code&gt; automatically fetching other pre-compiled binaries, your environment should be remarkably complete after this step.&#10;Restart your shell after it finishes.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;⚠️ Important: Customize!&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Fork the repository first!&lt;/strong&gt; Don&amp;rsquo;t use my configuration directly, especially &lt;code&gt;configs/git/gitconfig&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Update &lt;code&gt;configs/git/gitconfig&lt;/code&gt; with &lt;em&gt;your&lt;/em&gt; name and email.&lt;/li&gt;&#10;&lt;li&gt;Review &lt;code&gt;install.conf.yaml&lt;/code&gt; – remove links or shell commands you don&amp;rsquo;t need. You can see the full list of Python tools installed via &lt;code&gt;uv&lt;/code&gt; &lt;a href="https://github.com/basnijholt/dotfiles/blob/main/install.conf.yaml" target="_blank" rel="noopener"&gt;here&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;Modify &lt;code&gt;configs/shell/&lt;/code&gt; files to suit your workflow.&lt;/li&gt;&#10;&lt;li&gt;Adjust the &lt;code&gt;dotbins&lt;/code&gt; configuration (&lt;code&gt;submodules/dotbins/dotbins.yaml&lt;/code&gt;) to include &lt;em&gt;your&lt;/em&gt; favorite tools.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="a-note-on-secrets"&gt;A Note on Secrets &lt;a class="anchor" href="#a-note-on-secrets" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Sensitive information (API keys, etc.) is &lt;em&gt;not&lt;/em&gt; stored directly in the public repository.&#10;I use a private Git repository included as a submodule (&lt;code&gt;secrets/&lt;/code&gt;).&#10;This requires SSH authentication.&#10;Within that private repo, I use &lt;code&gt;gpg&lt;/code&gt; and &lt;a href="https://github.com/sobolevn/git-secret" target="_blank" rel="noopener"&gt;git-secret&lt;/a&gt; to encrypt sensitive files, adding another layer of security.&#10;You&amp;rsquo;ll need your own strategy for managing secrets, whether it&amp;rsquo;s a similar private repo, environment variables, or a dedicated secrets manager.&lt;/p&gt;&#10;&lt;h2 id="conclusion"&gt;Conclusion &lt;a class="anchor" href="#conclusion" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This dotfiles setup represents my ongoing effort to build a productive, consistent, and enjoyable terminal environment across a diverse range of machines – from personal laptops to &lt;a href="https://www.nijho.lt/post/homelab/"&gt;homelab servers&lt;/a&gt; and cloud instances.&#10;It solves real problems I&amp;rsquo;ve encountered in maintaining that consistency.&#10;By sharing it, I hope to offer some practical ideas and perhaps save others some time in crafting their own ideal setup.&lt;/p&gt;&#10;&lt;p&gt;Feel free to explore the &lt;a href="https://github.com/basnijholt/dotfiles" target="_blank" rel="noopener"&gt;repository&lt;/a&gt;, borrow what you find useful, and adapt it to your needs.&#10;If you find it helpful, consider giving it a star!&#10;If you have suggestions or find issues, contributions are welcome!&lt;/p&gt;&#10;&lt;p&gt;Happy terminal tinkering!&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;&lt;strong&gt;Further Reading &amp;amp; Links:&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles" target="_blank" rel="noopener"&gt;My Dotfiles Repository on GitHub&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Blog Post: &lt;a href="https://www.nijho.lt/post/homelab/"&gt;My Homelab Setup&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Blog Post: &lt;a href="https://www.nijho.lt/post/terminal-ninja/"&gt;Be a Ninja in the Terminal 🥷&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Blog Post: &lt;a href="https://www.nijho.lt/post/dotbins/"&gt;dotbins: Managing Binary Tools in Your Dotfiles 🧰&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Blog Post: &lt;a href="https://www.nijho.lt/post/ssh-1password-funtoo-keychain/"&gt;Combining Keychain and 1Password CLI for SSH Agent Management&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/anishathalye/dotbot" target="_blank" rel="noopener"&gt;Dotbot&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;Dotbins&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/sobolevn/git-secret" target="_blank" rel="noopener"&gt;Git Secret&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://iterm2.com/" target="_blank" rel="noopener"&gt;iTerm2&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>dotbins: managing binary tools in your dotfiles 🧰</title><link>https://www.nijho.lt/post/dotbins/</link><pubDate>Tue, 11 Mar 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/dotbins/</guid><description>&lt;p&gt;In my &lt;a href="../terminal-ninja/"&gt;previous post about terminal productivity&lt;/a&gt;, I shared a setup combining tools like &lt;code&gt;zsh&lt;/code&gt;, &lt;code&gt;oh-my-zsh&lt;/code&gt;, &lt;code&gt;zsh-z&lt;/code&gt;, and various plugins to create a highly efficient command-line environment. While that setup served me well for years, I&amp;rsquo;ve recently encountered a challenge: incorporating modern, compiled CLI tools across all my environments without administrative privileges.&lt;/p&gt;&#10;&lt;p&gt;Today, I&amp;rsquo;m excited to &lt;a href="https://github.com/basnijholt/dotbins/" target="_blank" rel="noopener"&gt;introduce &lt;strong&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - a solution I created to solve this exact problem.&lt;/p&gt;&#10;&lt;h2 id="the-modern-cli-tools-problem"&gt;The Modern CLI Tools Problem &lt;a class="anchor" href="#the-modern-cli-tools-problem" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over the past few years, we&amp;rsquo;ve seen an explosion of incredible, modern CLI tools written in languages like Rust and Go.&#10;Tools like:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/ajeetdsouza/zoxide" target="_blank" rel="noopener"&gt;&lt;code&gt;zoxide&lt;/code&gt;&lt;/a&gt; (a smarter, faster alternative to &lt;code&gt;zsh-z&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/sharkdp/bat" target="_blank" rel="noopener"&gt;&lt;code&gt;bat&lt;/code&gt;&lt;/a&gt; (a &lt;code&gt;cat&lt;/code&gt; replacement with syntax highlighting)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/eza-community/eza" target="_blank" rel="noopener"&gt;&lt;code&gt;eza&lt;/code&gt;&lt;/a&gt; (a modern &lt;code&gt;ls&lt;/code&gt; alternative)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/dandavison/delta" target="_blank" rel="noopener"&gt;&lt;code&gt;delta&lt;/code&gt;&lt;/a&gt; (a better diff viewer for Git)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/junegunn/fzf" target="_blank" rel="noopener"&gt;&lt;code&gt;fzf&lt;/code&gt;&lt;/a&gt; (a fuzzy finder for your terminal)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/BurntSushi/ripgrep" target="_blank" rel="noopener"&gt;&lt;code&gt;ripgrep&lt;/code&gt;&lt;/a&gt; (a faster, better &lt;code&gt;grep&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/atuinsh/atuin" target="_blank" rel="noopener"&gt;&lt;code&gt;atuin&lt;/code&gt;&lt;/a&gt; (a shell history with search and sync)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;These tools significantly enhance terminal productivity, but they come with a catch: &lt;strong&gt;they&amp;rsquo;re compiled binaries&lt;/strong&gt;, not simple scripts.&#10;While my previous setup worked well because all the tools were basic shell scripts I could include as submodules in my dotfiles repository, these modern alternatives require installation.&lt;/p&gt;&#10;&lt;p&gt;This became frustrating when working on:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Remote systems where I lacked admin/sudo permissions&lt;/li&gt;&#10;&lt;li&gt;Machines that I use only temporarily&lt;/li&gt;&#10;&lt;li&gt;Systems where I didn&amp;rsquo;t want to install package managers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;d spend time carefully setting up my dotfiles with all my configurations, only to find that the actual tools they relied on weren&amp;rsquo;t available.&lt;/p&gt;&#10;&lt;h2 id="enter-dotbins-"&gt;Enter &lt;code&gt;dotbins&lt;/code&gt; 🧰 &lt;a class="anchor" href="#enter-dotbins-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/dotbins/" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt; was born out of this frustration.&#10;It&amp;rsquo;s a utility that manages pre-compiled binaries for CLI tools across multiple platforms and architectures, right in your dotfiles repository.&lt;/p&gt;&#10;&lt;p&gt;The key insight: &lt;strong&gt;track pre-compiled binaries in a separate Git repository&lt;/strong&gt; (using Git LFS for efficiency), include this repository as a submodule in your dotfiles, and ensure all your essential tools are immediately available on any system.&lt;/p&gt;&#10;&lt;p&gt;No package manager, no sudo, no problem.&lt;/p&gt;&#10;&lt;h2 id="how-dotbins-works"&gt;How &lt;code&gt;dotbins&lt;/code&gt; Works &lt;a class="anchor" href="#how-dotbins-works" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;dotbins&lt;/code&gt; handles several critical tasks:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Downloads&lt;/strong&gt; binaries directly from GitHub releases for your selected tools&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Organizes&lt;/strong&gt; them by platform (macOS, Linux) and architecture (amd64, arm64)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Tracks&lt;/strong&gt; installed versions and update timestamps&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Extracts&lt;/strong&gt; binaries from archives in various formats&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Integrates&lt;/strong&gt; seamlessly with your shell via a simple PATH addition&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The configuration is straightforward - a YAML file where you define which tools you want and how to get them:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ripgrep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;BurntSushi/ripgrep&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;extract_binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;rg&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ripgrep-*/rg&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;asset_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;linux&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ripgrep-{version}-{arch}-unknown-linux-musl.tar.gz&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ripgrep-{version}-{arch}-apple-darwin.tar.gz&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;arch_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;amd64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;x86_64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;arm64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;aarch64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="my-workflow-a-two-repository-approach"&gt;My Workflow: A Two-Repository Approach &lt;a class="anchor" href="#my-workflow-a-two-repository-approach" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach uses two repositories:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Private dotfiles repository&lt;/strong&gt; containing all my configurations&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Public &lt;a href="https://github.com/basnijholt/mydotbins" target="_blank" rel="noopener"&gt;mydotbins repository&lt;/a&gt;&lt;/strong&gt; containing all the binaries&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I include &lt;code&gt;mydotbins&lt;/code&gt; as a submodule in my dotfiles repository.&#10;This separation keeps large binary files out of my main dotfiles repository and allows me to share the binary collection publicly while keeping my personal configurations private.&lt;/p&gt;&#10;&lt;p&gt;When I clone my dotfiles on a new system, I get not just my configuration files, but also all the CLI tools I depend on for productivity.&lt;/p&gt;&#10;&lt;h2 id="modern-alternatives-to-my-previous-setup"&gt;Modern Alternatives to My Previous Setup &lt;a class="anchor" href="#modern-alternatives-to-my-previous-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With &lt;code&gt;dotbins&lt;/code&gt;, I&amp;rsquo;ve been able to upgrade several tools from my previous setup:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Old Tool&lt;/th&gt;&#10;&lt;th&gt;Modern Alternative&lt;/th&gt;&#10;&lt;th&gt;Advantage&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/agkozak/zsh-z" target="_blank" rel="noopener"&gt;&lt;code&gt;zsh-z&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/ajeetdsouza/zoxide" target="_blank" rel="noopener"&gt;&lt;code&gt;zoxide&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Faster, smarter, works across shells&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;cat&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/sharkdp/bat" target="_blank" rel="noopener"&gt;&lt;code&gt;bat&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Syntax highlighting, Git integration&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;ls&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/eza-community/eza" target="_blank" rel="noopener"&gt;&lt;code&gt;eza&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Better formatting, Git integration&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/BurntSushi/ripgrep" target="_blank" rel="noopener"&gt;&lt;code&gt;ripgrep&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Much faster, respects &lt;code&gt;.gitignore&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Control + R&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/atuinsh/atuin" target="_blank" rel="noopener"&gt;&lt;code&gt;atuin&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Shell history with search and sync&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Default Git diff&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/dandavison/delta" target="_blank" rel="noopener"&gt;&lt;code&gt;delta&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Syntax highlighting, better visual diffs&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;N/A&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/junegunn/fzf" target="_blank" rel="noopener"&gt;&lt;code&gt;fzf&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fuzzy finding makes everything better&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;N/A&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/atuinsh/atuin" target="_blank" rel="noopener"&gt;&lt;code&gt;atuin&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Shell history with search and sync&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The best part? All of these are now automatically available on every machine where I clone my dotfiles, without any additional installation steps.&lt;/p&gt;&#10;&lt;h2 id="getting-started-with-dotbins"&gt;Getting Started with &lt;code&gt;dotbins&lt;/code&gt; &lt;a class="anchor" href="#getting-started-with-dotbins" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;If you&amp;rsquo;d like to adopt this approach, here&amp;rsquo;s how to get started:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install &lt;code&gt;dotbins&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install dotbins&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or better yet, use uv:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx dotbins&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Create a configuration file at &lt;code&gt;~/.config/dotbins/config.yaml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tools_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;~/.mydotbins/tools&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;linux&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;amd64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;arm64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;arm64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Add your favorite tools here&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fzf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;junegunn/fzf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;extract_binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fzf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fzf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;asset_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fzf-{version}-{platform}_{arch}.tar.gz&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;platform_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;darwin&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Initialize and download your tools:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotbins init&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotbins update&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Add the shell integration to your &lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.bashrc&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# dotbins - Add platform-specific binaries to PATH&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;_os&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;uname -s &lt;span class="p"&gt;|&lt;/span&gt; tr &lt;span class="s1"&gt;&amp;#39;[:upper:]&amp;#39;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;[:lower:]&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_os&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;darwin&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;_os&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;macos&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;_arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;uname -m&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86_64&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;_arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;amd64&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aarch64&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;arm64&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;_arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;arm64&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.mydotbins/tools/&lt;/span&gt;&lt;span class="nv"&gt;$_os&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;(Optional) Track your binaries in a separate Git repository with Git LFS for efficiency&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="finding-new-tools-to-add"&gt;Finding New Tools to Add &lt;a class="anchor" href="#finding-new-tools-to-add" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Not sure which pattern to use for a new tool? The &lt;code&gt;analyze&lt;/code&gt; command is here to help:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotbins analyze sharkdp/bat&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will suggest a configuration for the tool based on its GitHub release assets.&lt;/p&gt;&#10;&lt;h2 id="conclusion-the-evolution-of-my-terminal-setup"&gt;Conclusion: The Evolution of My Terminal Setup &lt;a class="anchor" href="#conclusion-the-evolution-of-my-terminal-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With &lt;a href="https://github.com/basnijholt/dotbins/" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt;, my terminal productivity setup has evolved beyond what I described in my &lt;a href="../terminal-ninja/"&gt;previous post&lt;/a&gt;.&#10;I can now seamlessly incorporate the best modern CLI tools without worrying about installation or administrative privileges.&lt;/p&gt;&#10;&lt;p&gt;The beauty of this approach is its portability—I get a consistent, powerful environment on any machine where I clone my dotfiles.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re a terminal power user who works across multiple environments, give &lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt; a try.&#10;It might just be the missing piece in your dotfiles setup!&lt;/p&gt;&#10;</description></item><item><title>GPL for Python packaging ⚖️</title><link>https://www.nijho.lt/post/gpl/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/gpl/</guid><description>&lt;h2 id="understanding-gpl-for-python-packaging"&gt;Understanding GPL for Python Packaging &lt;a class="anchor" href="#understanding-gpl-for-python-packaging" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over the last 10 years–since I started working on and with open source projects–the GPL topic has frequently come up with friends and colleagues.&#10;I simply write this post as a reminder to myself and to have something concise and clear to link to later.&#10;Also, &lt;a href="https://en.wikipedia.org/wiki/IANAL" target="_blank" rel="noopener"&gt;IANAL&lt;/a&gt;, so if you are relying on this post, please do your own research, I have included all the links I used to make this post.&lt;/p&gt;&#10;&lt;p&gt;The GNU General Public License (GPL) is widely used in the open-source world, but its implications can be unclear, especially regarding Python packaging.&#10;If you develop or distribute Python software, understanding GPL variants is crucial.&lt;/p&gt;&#10;&lt;h3 id="what-is-gpl"&gt;What is GPL? &lt;a class="anchor" href="#what-is-gpl" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;GPL is a strong copyleft license—meaning software using GPL-licensed code must also be released under the GPL.&#10;There are several key variants relevant to Python:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;GPLv2 &amp;amp; GPLv3&lt;/strong&gt;:&#10;These licenses require that software using GPL code (directly or indirectly) also be open-sourced under GPL upon distribution.(&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;)&#10;If your Python application imports or depends on a GPL package, your code must adopt GPL licensing.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;LGPL (Lesser GPL)&lt;/strong&gt;:&#10;LGPL is more permissive and is usually applied to libraries.&#10;It allows proprietary software to link or import the library without forcing the proprietary software itself to adopt LGPL.(&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;)&#10;You just need to ensure users can replace or modify the LGPL component.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;AGPL (Affero GPL)&lt;/strong&gt;:&#10;AGPL is similar to GPLv3 but includes a critical extra clause: it applies even when users access the software remotely over a network (such as web services or APIs).&#10;If you use an AGPL-licensed Python library, your entire application must be open-sourced under AGPL, even if you&amp;rsquo;re only hosting it online.(&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="what-does-linking-mean-in-python"&gt;What Does &amp;ldquo;Linking&amp;rdquo; Mean in Python? &lt;a class="anchor" href="#what-does-linking-mean-in-python" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;GPL licenses often refer to &amp;ldquo;linking,&amp;rdquo; traditionally associated with compiled languages (e.g., C, C++).&#10;Python doesn&amp;rsquo;t link code in the traditional sense.&#10;However, it is generally agreed upon that importing a Python module counts as linking for licensing purposes.(&lt;sup id="fnref1:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;)&#10;Therefore, directly importing a GPL-licensed Python package typically triggers the GPL&amp;rsquo;s copyleft requirements.&lt;/p&gt;&#10;&lt;h3 id="why-does-this-matter-for-python-packaging"&gt;Why Does This Matter for Python Packaging? &lt;a class="anchor" href="#why-does-this-matter-for-python-packaging" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Python makes it easy to incorporate third-party libraries.&#10;But unintentionally using GPL-licensed libraries (especially GPL or AGPL) legally obligates you to open-source your proprietary code.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Direct Dependencies&lt;/strong&gt;:&#10;If you directly import a GPL-licensed Python library, your software must be GPL-licensed upon distribution.(&lt;sup id="fnref2:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref1:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Indirect (Transitive) Dependencies&lt;/strong&gt;:&#10;If any dependency in your Python project indirectly relies on GPL-licensed code, your entire software must also adopt GPL licensing upon distribution.&#10;Even a hidden, indirect GPL dependency triggers the GPL requirements.(&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:10"&gt;&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref"&gt;10&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="recommendations"&gt;Recommendations &lt;a class="anchor" href="#recommendations" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Check Licenses Carefully&lt;/strong&gt;:&#10;Always verify the licenses of both your direct and indirect dependencies to prevent unintended GPL licensing obligations.(&lt;sup id="fnref:11"&gt;&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref"&gt;11&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref1:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Use Permissive or LGPL Licenses&lt;/strong&gt;:&#10;When planning to keep your own software proprietary, prioritize libraries with permissive licenses (e.g., MIT, Apache, BSD) or LGPL-licensed libraries, as they allow for proprietary use without triggering GPL requirements.(&lt;sup id="fnref1:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref1:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Separate GPL Code&lt;/strong&gt;:&#10;If you must utilize GPL code, explicitly separate it from proprietary code.&#10;Using subprocess calls or isolated microservices is often recommended since it counts as &amp;ldquo;mere aggregation,&amp;rdquo; not linking, thus avoiding GPL obligations.(&lt;sup id="fnref:12"&gt;&lt;a href="#fn:12" class="footnote-ref" role="doc-noteref"&gt;12&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref2:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;By clearly understanding GPL licensing implications, you can confidently manage your Python project&amp;rsquo;s dependencies and safely distribute your software.&lt;/p&gt;&#10;&lt;!-- Footnotes --&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL" target="_blank" rel="noopener"&gt;GNU GPL FAQ - If a library is released under the GPL&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref2:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/35/if-i-include-some-gpl-code-in-my-project-can-i-release-it-as-bsd" target="_blank" rel="noopener"&gt;Stack Exchange - If I include some GPL code in my project, can I release it as BSD?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/lgpl-3.0.en.html" target="_blank" rel="noopener"&gt;GNU Lesser General Public License&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/6831/can-i-use-gpl-3-0-or-lgpl-licensed-libraries-in-my-commercial-closed-source-iot" target="_blank" rel="noopener"&gt;Stack Exchange - Can I use GPL/LGPL libraries in commercial closed source projects?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank" rel="noopener"&gt;GNU Affero General Public License&lt;/a&gt;&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:6"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/11826/can-i-use-a-agpl-gpl-package-and-software-for-closed-source-backend-api" target="_blank" rel="noopener"&gt;Stack Exchange - Can I use AGPL/GPL package for closed source backend API?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:7"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/6033/can-a-non-gpl-python-program-use-gpl-python-module" target="_blank" rel="noopener"&gt;Stack Exchange - Can a non-GPL Python program use a GPL Python module?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref2:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:8"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility" target="_blank" rel="noopener"&gt;GNU GPL FAQ - License Compatibility&lt;/a&gt;&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:9"&gt;&#10;&lt;p&gt;&lt;a href="https://dev.to/wagenrace/8000-python-packages-might-have-to-change-to-gnu-general-public-license-1000" target="_blank" rel="noopener"&gt;Dev.to - 8000 Python packages might have to change to GNU General Public License&lt;/a&gt;&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:10"&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/6748333/import-a-library-that-imports-a-gpl-library" target="_blank" rel="noopener"&gt;Stack Overflow - Import a library that imports a GPL library&lt;/a&gt;&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:11"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/license-list.html" target="_blank" rel="noopener"&gt;GNU License List&lt;/a&gt;&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:12"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/gpl-faq.html#MereAggregation" target="_blank" rel="noopener"&gt;GNU GPL FAQ - What is &amp;ldquo;mere aggregation&amp;rdquo;?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:12" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>tuitorial: terminal-based interactive code tutorials 📚</title><link>https://www.nijho.lt/post/tuitorial/</link><pubDate>Sat, 04 Jan 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/tuitorial/</guid><description>&lt;p&gt;While trying to demonstrate my &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt; package&lt;/a&gt; (a tool for automating computational pipelines), I hit a common presentation problem: how do you effectively walk through code without endless repetition?&lt;/p&gt;&#10;&lt;p&gt;Traditional presentation tools like PowerPoint force you to copy-paste the same code snippets multiple times just to highlight different parts. Jupyter notebooks are great for self-paced learning but clunky for live presentations (too much text that the viewer will try to read). I needed something that would let me define code once and create multiple views highlighting different aspects.&lt;/p&gt;&#10;&lt;p&gt;So, during the holiday season, I did what any reasonable developer would do: procrastinated on my actual work and built a new tool instead!&lt;/p&gt;&#10;&lt;h2 id="what-is-tuitorial"&gt;What is tuitorial? &lt;a class="anchor" href="#what-is-tuitorial" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; is a Python package that lets you create interactive, terminal-based code tutorials where you define the code ONCE, then create steps that highlight different parts based on rules (like regex patterns or line numbers). No more copy-paste presentations!&lt;/p&gt;&#10;&lt;h2 id="key-features"&gt;Key Features &lt;a class="anchor" href="#key-features" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Write code once&lt;/strong&gt;: Define your code once, then create multiple views highlighting different aspects&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Rich highlighting options&lt;/strong&gt;: From simple literal matches to complex regex patterns&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Interactive navigation&lt;/strong&gt;: Intuitive keyboard controls for stepping through tutorials&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Beautiful terminal UI&lt;/strong&gt;: Powered by Textual for a polished look&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Flexible configuration&lt;/strong&gt;: Choose between Python API or YAML format&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Development mode&lt;/strong&gt;: Live reloading for quick iterations&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="see-it-in-action"&gt;See It in Action &lt;a class="anchor" href="#see-it-in-action" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a demo of &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; explaining &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; - a package that lets you create computational pipelines with automatic parallelization, caching, and dependency management:&lt;/p&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/reduced-motion.3dfea0061c17c60a9a200c61ef1e3b3e98d5bdc4eaa4e0a64b3ed2a4d13c72fd.js" integrity="sha256-Pf6gBhwXxgqaIAxh7x47PpjVvcTqpOCmSz7SpNE8cv0=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;video controls autoplay muted playsinline loop preload="metadata"&gt;&#10;&lt;source src="https://www.nijho.lt/post/tuitorial/tuitorial-0.4.0.mp4" type="video/mp4"&gt;&#10;&lt;/video&gt;&#10;&#10;&lt;h2 id="who-should-use-this"&gt;Who Should Use This? &lt;a class="anchor" href="#who-should-use-this" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; isn&amp;rsquo;t for everyone, it&amp;rsquo;s perfect for:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Developers giving technical presentations&lt;/li&gt;&#10;&lt;li&gt;Teachers explaining code concepts&lt;/li&gt;&#10;&lt;li&gt;Teams creating interactive documentation&lt;/li&gt;&#10;&lt;li&gt;Anyone who loves terminal-based tools&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="getting-started"&gt;Getting Started &lt;a class="anchor" href="#getting-started" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Have &lt;code&gt;uv&lt;/code&gt; installed? Run the following command to see a quick example:&#10;&lt;code&gt;uvx tuitorial https://raw.githubusercontent.com/basnijholt/tuitorial/refs/heads/main/examples/pipefunc.yaml&lt;/code&gt;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install tuitorial&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check out the &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt; for examples and detailed usage instructions.&lt;/p&gt;&#10;&lt;h2 id="final-thoughts"&gt;Final Thoughts &lt;a class="anchor" href="#final-thoughts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Building &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; was a lot of fun!&#10;While it may not replace PowerPoint for your next business presentation, it offers a unique approach to code demonstrations that I hope others will find useful.&lt;/p&gt;&#10;&lt;p&gt;Try it out and let me know what you think!&lt;/p&gt;&#10;</description></item><item><title>Home Assistant Ecosystem 🏠</title><link>https://www.nijho.lt/post/advent-of-open-source/24-home-assistant-ecosystem/</link><pubDate>Tue, 24 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/24-home-assistant-ecosystem/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;There are not enough days in this advent calendar to highlight all my projects. So today is a collection of IoT/Home Assistant-related packages. Each has its own story.&lt;/p&gt;&#10;&lt;h2 id="-origin-stories--technical-details"&gt;📖 Origin Stories &amp;amp; Technical Details &lt;a class="anchor" href="#-origin-stories--technical-details" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My Home Assistant obsession began as described in &lt;a href="https://www.linkedin.com/posts/basnijholt_opensource-homeassistant-smarthome-activity-7272248713882820608-w7RV" target="_blank" rel="noopener"&gt;Day 10&lt;/a&gt;. Many projects emerged from this hobby:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/aiokef" target="_blank" rel="noopener"&gt;&lt;strong&gt;aiokef&lt;/strong&gt;&lt;/a&gt;: To control my KEF speakers, I reverse-engineered network traffic and decoded raw byte sequences, diving deep into proprietary protocols to &amp;ldquo;speak&amp;rdquo; the speaker&amp;rsquo;s language.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/miflora" target="_blank" rel="noopener"&gt;&lt;strong&gt;miflora&lt;/strong&gt;&lt;/a&gt;: When &lt;a href="https://github.com/ChristianKuehnel" target="_blank" rel="noopener"&gt;@ChristianKuehnel&lt;/a&gt; couldn&amp;rsquo;t maintain this plant sensor library anymore, he transferred it to me. A perfect example of open source collaboration, it continues to help users monitor plant health and demonstrates how projects can thrive through community handovers.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/addon-otmonitor" target="_blank" rel="noopener"&gt;&lt;strong&gt;addon-otmonitor&lt;/strong&gt;&lt;/a&gt;: This HA add-on packages OpenTherm Monitor for the OpenTherm Gateway (OTGW). Using Docker to simplify deployment, it&amp;rsquo;s become a powerful open-source alternative to Nest or Ecobee, giving full control over heating systems.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/home-assistant-macbook-touch-bar" target="_blank" rel="noopener"&gt;&lt;strong&gt;home-assistant-macbook-touch-bar&lt;/strong&gt;&lt;/a&gt;: Remember that useless touch bar on the old MacBook? I transformed it into a mini Home Assistant control panel for lights, temperature, and more - a classic case of hardware hacking giving new life to abandoned technology.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/python-kasa/python-kasa" target="_blank" rel="noopener"&gt;&lt;strong&gt;python-kasa&lt;/strong&gt;&lt;/a&gt;: To modernize the popular &lt;code&gt;pyHS100&lt;/code&gt; library for TP-Link devices, I converted it to use &lt;code&gt;asyncio&lt;/code&gt;. With &lt;a href="https://github.com/rytilahti" target="_blank" rel="noopener"&gt;@rytilahti&lt;/a&gt;, we created the &lt;code&gt;python-kasa&lt;/code&gt; organization through community collaboration. While I haven&amp;rsquo;t contributed much since its inception, it&amp;rsquo;s now the standard for controlling Kasa devices.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact--lessons"&gt;📊 Impact &amp;amp; Lessons &lt;a class="anchor" href="#-impact--lessons" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;These projects serve thousands of users while teaching valuable lessons:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Open source empowers us: from KEF speaker integration to plant monitoring&lt;/li&gt;&#10;&lt;li&gt;Reverse engineering unlocks potential: turning proprietary protocols into open solutions&lt;/li&gt;&#10;&lt;li&gt;Community drives success: projects like miflora and python-kasa thrive through collaboration&lt;/li&gt;&#10;&lt;li&gt;Innovation through repurposing: giving new life to hardware like the MacBook Touch Bar&lt;/li&gt;&#10;&lt;li&gt;Modern tech matters: Docker simplifies deployment, async code boosts performance&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want to explore these projects? Check them out on GitHub:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/aiokef" target="_blank" rel="noopener"&gt;aiokef&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/miflora" target="_blank" rel="noopener"&gt;miflora&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/addon-otmonitor" target="_blank" rel="noopener"&gt;addon-otmonitor&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/home-assistant-macbook-touch-bar" target="_blank" rel="noopener"&gt;home-assistant-macbook-touch-bar&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/python-kasa/python-kasa" target="_blank" rel="noopener"&gt;python-kasa&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This concludes our Advent of Open Source! I hope you&amp;rsquo;ve enjoyed this journey through my projects as much as I&amp;rsquo;ve enjoyed sharing them. Happy holidays and happy coding!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #IoT #Python #SmartHome&lt;/p&gt;&#10;</description></item><item><title>pfapack 🔢</title><link>https://www.nijho.lt/post/advent-of-open-source/23-pfapack/</link><pubDate>Mon, 23 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/23-pfapack/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever needed to calculate the square root of the determinant of a skew-symmetric matrix? Probably not, but if you&amp;rsquo;re in quantum physics, you might have! This is called the Pfaffian. Today&amp;rsquo;s project, &lt;a href="https://github.com/basnijholt/pfapack" target="_blank" rel="noopener"&gt;&lt;code&gt;pfapack&lt;/code&gt;&lt;/a&gt;, is a Python package that provides efficient routines for computing it. The underlying algorithms and original code implementations were developed by my PhD co-promotor Michael Wimmer. I created the &lt;code&gt;pfapack&lt;/code&gt; package to make this powerful tool easily accessible.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Working in quantum physics, I frequently need to compute the Pfaffian. Michael had not only developed the algorithms but also wrote highly optimized C, FORTRAN, and Python implementations, distributing them as a tar file &lt;a href="https://michaelwimmer.org/downloads.html" target="_blank" rel="noopener"&gt;on his website&lt;/a&gt;. To make it easier for researchers working in Python, I packaged his existing Python code, added wrappers for his C and FORTRAN implementations, and set up automated builds for PyPI and conda-forge to make it easily installable.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Python Wrapper for Efficient Code:&lt;/strong&gt; User-friendly Python interface to Wimmer&amp;rsquo;s optimized C and FORTRAN.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Multiple Algorithms:&lt;/strong&gt; Supports both the Parlett-Reid and Householder methods.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Optimized for Performance:&lt;/strong&gt; Leverages low-level optimizations for speed.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Conda and Pip Installable:&lt;/strong&gt; Easy installation via &lt;code&gt;conda-forge&lt;/code&gt; and PyPI.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Automated Wheel Builds:&lt;/strong&gt; Uses &lt;a href="https://github.com/pypa/cibuildwheel" target="_blank" rel="noopener"&gt;&lt;code&gt;pypa/cibuildwheel&lt;/code&gt;&lt;/a&gt; for streamlined, automated builds and releases.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cross-Platform Compatibility:&lt;/strong&gt; Works on Linux, macOS, and Windows (with some caveats).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Makes specialized mathematical algorithms readily available to Python users.&lt;/li&gt;&#10;&lt;li&gt;Simplifies the use of highly optimized code.&lt;/li&gt;&#10;&lt;li&gt;15 GitHub stars.&lt;/li&gt;&#10;&lt;li&gt;Used in research projects that require efficient Pfaffian computations, particularly in quantum physics.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Bridging Languages:&lt;/strong&gt; Wrapping C and FORTRAN for Python integration.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Automated Builds:&lt;/strong&gt; Setting up automated builds for multiple platforms was complex.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Windows Compatibility:&lt;/strong&gt; As always Windows support is a PITA; it currently requires MSYS2 and MinGW64.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Making specialized tools accessible broadens their impact.&lt;/li&gt;&#10;&lt;li&gt;Wrapping low-level code in Python can improve usability.&lt;/li&gt;&#10;&lt;li&gt;Automated builds are essential for maintainability.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/pypa/cibuildwheel" target="_blank" rel="noopener"&gt;&lt;code&gt;pypa/cibuildwheel&lt;/code&gt;&lt;/a&gt; is awesome for cross-platform builds.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-credits"&gt;🙏 Credits &lt;a class="anchor" href="#-credits" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;All the credits for the underlying algorithms and original code implementations go to Michael Wimmer. I simply made it easier to distribute and use in Python.&lt;/p&gt;&#10;&lt;p&gt;Want to compute Pfaffians efficiently in Python? Check out &lt;a href="https://github.com/basnijholt/pfapack" target="_blank" rel="noopener"&gt;&lt;code&gt;pfapack&lt;/code&gt; on GitHub&lt;/a&gt; or install it via &lt;code&gt;pip install pfapack&lt;/code&gt; or &lt;code&gt;conda install -c conda-forge pfapack&lt;/code&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #ScientificComputing #Math #Pfaffian #FORTRAN #C&lt;/p&gt;&#10;</description></item><item><title>tiller-streamlit 💰</title><link>https://www.nijho.lt/post/advent-of-open-source/22-tiller-streamlit/</link><pubDate>Sun, 22 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/22-tiller-streamlit/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Tracking personal finances is crucial, especially when working towards financial independence (see my &lt;a href="../04-wenfire"&gt;WenFIRE post on Day 4&lt;/a&gt;). Today&amp;rsquo;s project is &lt;a href="https://github.com/basnijholt/tiller-streamlit" target="_blank" rel="noopener"&gt;&lt;code&gt;tiller-streamlit&lt;/code&gt;&lt;/a&gt;, a Streamlit app that visualizes your financial data from &lt;a href="https://www.tillerhq.com/" target="_blank" rel="noopener"&gt;Tiller&lt;/a&gt;. I host it on my local network under &lt;code&gt;money.local&lt;/code&gt; using Traefik for easy access.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Used to use Intuit Mint, but when they shut down, I needed an alternative with full data control.&lt;/li&gt;&#10;&lt;li&gt;Tiller automatically populates your financial data into a Google Sheet that you own forever.&lt;/li&gt;&#10;&lt;li&gt;With 20+ accounts, manual updates are tedious – Tiller automates it.&lt;/li&gt;&#10;&lt;li&gt;Combined Tiller&amp;rsquo;s data with Streamlit&amp;rsquo;s ease of use, creating a personal finance dashboard.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Powered by Streamlit: Turns Python into a web app with minimal code.&lt;/li&gt;&#10;&lt;li&gt;Tiller Integration: Connects to Tiller&amp;rsquo;s Google Sheets for automated data retrieval.&lt;/li&gt;&#10;&lt;li&gt;Interactive Visualizations: Uses Plotly and Altair for dynamic charts.&lt;/li&gt;&#10;&lt;li&gt;Customizable Analysis: Filter by categories, time periods, and more.&lt;/li&gt;&#10;&lt;li&gt;Net Worth Tracking: Calculates and visualizes net worth over time.&lt;/li&gt;&#10;&lt;li&gt;Dockerized Deployment: Easy deployment with Docker, and served locally with Traefik.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Primarily for personal use, it has significantly improved my understanding of my finances.&lt;/li&gt;&#10;&lt;li&gt;Great way to learn Streamlit and explore its capabilities.&lt;/li&gt;&#10;&lt;li&gt;Visualizing spending patterns helps identify areas to cut back and make informed financial decisions on the path to financial independence.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Learning Streamlit: Excellent documentation and community support made it a smooth process.&lt;/li&gt;&#10;&lt;li&gt;Data Wrangling: Transforming Tiller&amp;rsquo;s raw data into a format suitable for visualization.&lt;/li&gt;&#10;&lt;li&gt;Deployment: Docker and Traefik simplified local deployment, making it accessible on my home network.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Data Ownership Matters: Owning your financial data gives you control and flexibility.&lt;/li&gt;&#10;&lt;li&gt;Visualization is Key: Seeing your finances visually makes a huge difference.&lt;/li&gt;&#10;&lt;li&gt;Streamlit is Powerful: Simplifies web app development.&lt;/li&gt;&#10;&lt;li&gt;Automation is Essential: Tiller&amp;rsquo;s automatic data population makes the whole process sustainable.&lt;/li&gt;&#10;&lt;li&gt;Docker and Traefik Simplify Local Deployment: Making it easy to access the app on my home network.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-future-plans"&gt;🔮 Future Plans &lt;a class="anchor" href="#-future-plans" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Budgeting Features: Integrate budget tracking and forecasting.&lt;/li&gt;&#10;&lt;li&gt;Goal Setting: Set financial goals and track progress.&lt;/li&gt;&#10;&lt;li&gt;Investment Tracking: Incorporate investment data for a complete picture.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want to visualize your own finances with Tiller and Streamlit? Check out &lt;a href="https://github.com/basnijholt/tiller-streamlit" target="_blank" rel="noopener"&gt;&lt;code&gt;tiller-streamlit&lt;/code&gt; on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Streamlit #PersonalFinance #DataVisualization #Tiller #Traefik #Docker&lt;/p&gt;&#10;</description></item><item><title>Interactive Graph Visualization 🕸️</title><link>https://www.nijho.lt/post/advent-of-open-source/21-interactive-graph-visualization/</link><pubDate>Sat, 21 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/21-interactive-graph-visualization/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;After creating &lt;a href="../20-pipefunc"&gt;pipefunc (Day 20)&lt;/a&gt;, I wanted a way to visualize its (possibly large and complex) computational graphs interactively in Jupyter notebooks.&#10;What started as a simple visualization PR evolved into two standalone packages: &lt;a href="https://github.com/pipefunc/graphviz-anywidget" target="_blank" rel="noopener"&gt;graphviz-anywidget&lt;/a&gt; (Python) and &lt;a href="https://github.com/pipefunc/graphvizsvg" target="_blank" rel="noopener"&gt;graphvizsvg&lt;/a&gt; (JavaScript) - which unexpectedly introduced me to JavaScript development!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While working on pipefunc&amp;rsquo;s visualization features, I discovered that existing solutions didn&amp;rsquo;t quite meet my needs.&#10;However, I found several components that would solve my issue:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://anywidget.dev/" target="_blank" rel="noopener"&gt;AnyWidget&lt;/a&gt; (interactive GraphViz in Jupyter)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/hpcc-systems/hpcc-js-wasm" target="_blank" rel="noopener"&gt;@hpcc-js/wasm-graphviz&lt;/a&gt; (GraphViz in WASM)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/magjac/d3-graphviz" target="_blank" rel="noopener"&gt;d3-graphviz&lt;/a&gt; (interactive GraphViz in D3)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/mountainstorm/jquery.graphviz.svg/" target="_blank" rel="noopener"&gt;jquery.graphviz.svg&lt;/a&gt; (GraphViz SVG manipulation)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;AnyWidget would work with ESM modules, however, I had no clue what that even was.&#10;The &lt;code&gt;jquery.graphviz.svg&lt;/code&gt; package wasn&amp;rsquo;t one and hadn&amp;rsquo;t been maintained in 8 years.&lt;/p&gt;&#10;&lt;p&gt;With AI as my JavaScript mentor, I first rewrote &lt;code&gt;jquery.graphviz.svg&lt;/code&gt; into a modern ESM module (graphvizsvg), adding comprehensive tests that uncovered bugs in the original.&#10;Then, using AnyWidget, I created graphviz-anywidget to connect everything together - combining the WASM-powered d3-graphviz with the interactive features from graphvizsvg into a seamless Python package.&lt;/p&gt;&#10;&lt;p&gt;To my surprise, I found JavaScript&amp;rsquo;s development tooling remarkably sophisticated - especially npm, which handles everything from linting to testing to bundling to publishing.&#10;Something we&amp;rsquo;re still missing in Python!&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/pipefunc/graphviz-anywidget" target="_blank" rel="noopener"&gt;graphviz-anywidget&lt;/a&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Interactive Graphviz visualization in Jupyter&lt;/li&gt;&#10;&lt;li&gt;WASM-powered graph rendering&lt;/li&gt;&#10;&lt;li&gt;Directional graph traversal&lt;/li&gt;&#10;&lt;li&gt;Node and edge highlighting&lt;/li&gt;&#10;&lt;li&gt;Works in JupyterLab, Notebook, and VS Code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/pipefunc/graphvizsvg" target="_blank" rel="noopener"&gt;graphvizsvg&lt;/a&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Modern ESM module for interactive SVG visualization&lt;/li&gt;&#10;&lt;li&gt;Automatic color transitions&lt;/li&gt;&#10;&lt;li&gt;Bi-directional graph traversal&lt;/li&gt;&#10;&lt;li&gt;Comprehensive test coverage&lt;/li&gt;&#10;&lt;li&gt;My first npm package!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Enhances &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;pipefunc&lt;/a&gt;&amp;rsquo;s visualization capabilities&lt;/li&gt;&#10;&lt;li&gt;Modernizes legacy jQuery code for modern web development&lt;/li&gt;&#10;&lt;li&gt;Proves that diving into new technologies can yield useful tools&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Learning JavaScript/TypeScript from scratch&lt;/li&gt;&#10;&lt;li&gt;Making WASM work in Jupyter&lt;/li&gt;&#10;&lt;li&gt;Getting it to work in VS Code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Don&amp;rsquo;t let preconceptions limit your tech choices&lt;/li&gt;&#10;&lt;li&gt;Modern JavaScript development is actually enjoyable&lt;/li&gt;&#10;&lt;li&gt;AI can be an excellent programming mentor&lt;/li&gt;&#10;&lt;li&gt;Good visualization makes complex data accessible&lt;/li&gt;&#10;&lt;li&gt;Breaking features into separate packages increases reusability&lt;/li&gt;&#10;&lt;li&gt;Test coverage helps find bugs in even well-used code&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to visualize graphs interactively? Check out &lt;a href="https://github.com/pipefunc/graphviz-anywidget" target="_blank" rel="noopener"&gt;graphviz-anywidget&lt;/a&gt; and &lt;a href="https://github.com/pipefunc/graphvizsvg" target="_blank" rel="noopener"&gt;graphvizsvg&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #JavaScript #Python #DataVisualization #WebDev&lt;/p&gt;&#10;</description></item><item><title>pipefunc 🕸️</title><link>https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/</link><pubDate>Fri, 20 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/20-pipefunc/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Of all the projects I&amp;rsquo;m sharing this month, this one that started as a passion project excites me the most!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Sometimes the best projects are born from the most unexpected moments. More than 1½ year ago, while on parental leave with my twin boys, I found myself with small pockets of time during naps to work on a problem that had been bothering me for years: the tedious bookkeeping required in complex computational workflows. Every scientific computation project seemed to reinvent the same patterns - managing function dependencies, parameter sweeps, result caching, parallelization, and a lot of boilerplate to combine the resulting data. I wanted something that would let scientists focus on their science, not on pipeline management.&lt;/p&gt;&#10;&lt;figure id="figure-example-of-a-pipeline-generated-by-pipefunc"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_91068e582234f921.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_75ce53994a0e90e4.webp 400w, https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_91068e582234f921.webp 760w, https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_220beece6a403360.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="383" alt="A screenshot of a code editor showing Python code that uses the &amp;#39;pipefunc&amp;#39; library to define and visualize a computational pipeline. On the right, a graph generated by &amp;#39;pipeline.visualize()&amp;#39; is displayed, showing nodes and connections representing the pipeline&amp;#39;s structure." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Example of a pipeline generated by PipeFunc&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Automatic DAG construction via very simple and lightweight syntax&lt;/li&gt;&#10;&lt;li&gt;N-dimensional parameter sweeps with automatic parallelization&lt;/li&gt;&#10;&lt;li&gt;Visual pipeline representation using NetworkX&lt;/li&gt;&#10;&lt;li&gt;Resource profiling (CPU, memory, time)&lt;/li&gt;&#10;&lt;li&gt;Type validation between pipeline stages&lt;/li&gt;&#10;&lt;li&gt;Ultra-fast: only 15 µs overhead per function&lt;/li&gt;&#10;&lt;li&gt;Flexible caching strategies (memory, disk, cloud)&lt;/li&gt;&#10;&lt;li&gt;Integration with scientific computing tools:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://networkx.org/" target="_blank" rel="noopener"&gt;NetworkX&lt;/a&gt; for graph algorithms&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://numpy.org/" target="_blank" rel="noopener"&gt;NumPy&lt;/a&gt; for multi-dimensional arrays&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.xarray.dev/" target="_blank" rel="noopener"&gt;Xarray&lt;/a&gt; for labeled arrays&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://zarr.dev/" target="_blank" rel="noopener"&gt;Zarr&lt;/a&gt; for distributed storage&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://adaptive.readthedocs.io/" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt; for parallel computing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;230 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;700+ tests with 100% coverage&lt;/li&gt;&#10;&lt;li&gt;Fully typed codebase&lt;/li&gt;&#10;&lt;li&gt;Comprehensive documentation&lt;/li&gt;&#10;&lt;li&gt;Tested on real workflow on SLURM cluster&lt;/li&gt;&#10;&lt;li&gt;Covered on &lt;a href="https://pycoders.com/issues/650" target="_blank" rel="noopener"&gt;Pycoder&amp;rsquo;s Weekly&lt;/a&gt; with &amp;gt;100k subscribers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Balancing simplicity with power&lt;/li&gt;&#10;&lt;li&gt;Making complex workflows intuitive&lt;/li&gt;&#10;&lt;li&gt;Handling distributed computing edge cases&lt;/li&gt;&#10;&lt;li&gt;Ensuring type safety across the pipeline&lt;/li&gt;&#10;&lt;li&gt;Optimizing performance without sacrificing features&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes the best time to code is during baby naps&lt;/li&gt;&#10;&lt;li&gt;Complex problems can have elegant solutions&lt;/li&gt;&#10;&lt;li&gt;Scientific computing needs better tooling&lt;/li&gt;&#10;&lt;li&gt;Good abstractions make hard things easy&lt;/li&gt;&#10;&lt;li&gt;Type hints and tests prevent headaches&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="-future-plans"&gt;🔮 Future Plans &lt;a class="anchor" href="#-future-plans" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The journey is far from over. Plans include:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Enhanced cloud computing support&lt;/li&gt;&#10;&lt;li&gt;More interactive visualization options&lt;/li&gt;&#10;&lt;li&gt;Interactive pipeline debugging tools&lt;/li&gt;&#10;&lt;li&gt;Expanded parameter sweep capabilities&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want to simplify your computational workflows? Check out &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;pipefunc on GitHub&lt;/a&gt; or read the &lt;a href="https://pipefunc.readthedocs.io/" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DataScience #ScientificComputing #Programming&lt;/p&gt;&#10;</description></item><item><title>fileup 📤</title><link>https://www.nijho.lt/post/advent-of-open-source/19-fileup/</link><pubDate>Thu, 19 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/19-fileup/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the best tools are the ones you build for yourself and keep using for years. Today&amp;rsquo;s project is &lt;a href="https://github.com/basnijholt/fileup" target="_blank" rel="noopener"&gt;&lt;code&gt;fileup&lt;/code&gt;&lt;/a&gt;, a tiny command-line utility that&amp;rsquo;s been making my life easier for 7 years - because sharing files shouldn&amp;rsquo;t require leaving the terminal!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2017, I found myself repeatedly going through the same tedious process: needing to share a file, opening a browser, navigating to a file sharing service, waiting for uploads, and copying URLs. As someone who lives in the terminal, this felt inefficient. I wanted something as simple as &lt;code&gt;fu filename&lt;/code&gt; that would handle everything and put the URL in my clipboard. Seven years and countless file shares later, it&amp;rsquo;s still one of my most-used tools.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Supports both FTP and SCP uploads&lt;/li&gt;&#10;&lt;li&gt;Smart URL generation:&#10;&lt;ul&gt;&#10;&lt;li&gt;Jupyter notebooks → nbviewer links&lt;/li&gt;&#10;&lt;li&gt;Images → markdown-formatted links&lt;/li&gt;&#10;&lt;li&gt;Regular files → direct URLs&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Automatic file expiration and cleanup&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies - uses only Python standard library&lt;/li&gt;&#10;&lt;li&gt;Clipboard integration (macOS)&lt;/li&gt;&#10;&lt;li&gt;Simple INI configuration&lt;/li&gt;&#10;&lt;li&gt;Works as both a standalone script or installed package&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This is one of those tools where GitHub stars (currently 0!) don&amp;rsquo;t tell the whole story:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Used almost daily for 7 years&lt;/li&gt;&#10;&lt;li&gt;Shared thousands of files&lt;/li&gt;&#10;&lt;li&gt;Saved countless minutes of browser interactions&lt;/li&gt;&#10;&lt;li&gt;Proves that utility trumps popularity in open source&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Longevity comes from solving a real, persistent need&lt;/li&gt;&#10;&lt;li&gt;A tool doesn&amp;rsquo;t need to be revolutionary to be useful&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies mean zero maintenance headaches&lt;/li&gt;&#10;&lt;li&gt;Sometimes the impact of a tool isn&amp;rsquo;t measured in stars&lt;/li&gt;&#10;&lt;li&gt;Simple tools that remove friction are worth maintaining&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to share files from your terminal? Check out &lt;a href="https://github.com/basnijholt/fileup" target="_blank" rel="noopener"&gt;fileup on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DevTools #CommandLine #Programming&lt;/p&gt;&#10;</description></item><item><title>Psychedelic Data Science 🍄</title><link>https://www.nijho.lt/post/advent-of-open-source/18-psychedelic-data-science/</link><pubDate>Wed, 18 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/18-psychedelic-data-science/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;📝 Note: Running out of awesome projects and I want to save the best for last, so today just something fun.&lt;/p&gt;&#10;&lt;p&gt;While preparing for this advent calendar, I browsed through my 382 GitHub repositories and rediscovered this time capsule from 8 years ago. It explores the language of psychedelic experiences through data science - with an interesting hypothesis: do psychedelic experiences generate richer vocabulary compared to other substance reports?&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2015, when I was still &amp;ldquo;young&amp;rdquo; (before back pain and two-day hangovers became a thing), I was learning data science and natural language processing. I had an amusing hypothesis: surely people describing their psychedelic experiences would use richer vocabulary than those writing about stimulants - I mean, who hasn&amp;rsquo;t pondered the linguistic complexity of &amp;ldquo;everything is connected&amp;rdquo; versus &amp;ldquo;I cleaned my entire apartment at 4 AM&amp;rdquo;? Erowid.org, with its thousands of detailed first-person narratives across different substances, provided the perfect dataset to test this theory.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Natural language processing of experience reports&lt;/li&gt;&#10;&lt;li&gt;Vocabulary richness analysis across substance categories&lt;/li&gt;&#10;&lt;li&gt;TF-IDF vectorization for text analysis&lt;/li&gt;&#10;&lt;li&gt;Support Vector Machine classification of experience types&lt;/li&gt;&#10;&lt;li&gt;Web scraping with BeautifulSoup&lt;/li&gt;&#10;&lt;li&gt;K-means clustering for discovering common themes&lt;/li&gt;&#10;&lt;li&gt;Word cloud generation to visualize vocabulary differences&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A learning project that helped understand:&#10;&lt;ul&gt;&#10;&lt;li&gt;How different experiences shape language use&lt;/li&gt;&#10;&lt;li&gt;Processing subjective experience narratives&lt;/li&gt;&#10;&lt;li&gt;Document classification techniques&lt;/li&gt;&#10;&lt;li&gt;The challenges of quantifying vocabulary richness&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;10 GitHub stars&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Implementing respectful web scraping&lt;/li&gt;&#10;&lt;li&gt;Controlling for report length and education level&lt;/li&gt;&#10;&lt;li&gt;Creating meaningful metrics for vocabulary richness&lt;/li&gt;&#10;&lt;li&gt;Visualizing linguistic patterns across categories&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Putting things on GitHub means it&amp;rsquo;s less likely forgotten things get lost&lt;/li&gt;&#10;&lt;li&gt;Early data science projects often reveal our initial fascinations&lt;/li&gt;&#10;&lt;li&gt;Web scraping requires both technical skill and ethical consideration&lt;/li&gt;&#10;&lt;li&gt;Text analysis tools have evolved dramatically since 2015&lt;/li&gt;&#10;&lt;li&gt;Sometimes the most interesting projects are the ones you almost forgot about&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to explore this intersection of psychedelics, language, and data science? Check out &lt;a href="https://github.com/basnijholt/psychedelic-data-science" target="_blank" rel="noopener"&gt;the project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DataScience #NLP #MachineLearning&lt;/p&gt;&#10;</description></item><item><title>Adaptive Scheduler 🚀</title><link>https://www.nijho.lt/post/advent-of-open-source/17-adaptive-scheduler/</link><pubDate>Tue, 17 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/17-adaptive-scheduler/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever tried to efficiently run 100,000 cores from a Jupyter notebook? No? Well, I have, and it led to some interesting discoveries about the limits of parallel computing tools.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;During my 2018 internship at Microsoft Quantum, we faced a unique challenge: our &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;adaptive sampling algorithms&lt;/a&gt; needed to run on massive computing clusters (50,000+ cores), but existing tools couldn&amp;rsquo;t handle it efficiently.&lt;/p&gt;&#10;&lt;p&gt;The problem was interesting: traditional parallel computing tools like Dask rely on a central scheduler process. Imagine 100,000 cores, each finishing a task every 10 seconds. That means every 10 microseconds, a core needs new work! With typical scheduler overhead being 1-50 milliseconds, workers would spend 99% of their time waiting rather than computing.&lt;/p&gt;&#10;&lt;p&gt;Usually, you&amp;rsquo;d solve this by batching work ahead of time. But with adaptive sampling (&lt;a href="../16-adaptive"&gt;yesterday&amp;rsquo;s project&lt;/a&gt;), that&amp;rsquo;s impossible - you need the results of previous calculations to know what to compute next. It was like trying to conduct an orchestra where each musician needs personal instructions every few seconds!&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;We created a solution that:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Acts as a meta-scheduler, launching multiple sub-schedulers (&lt;a href="https://www.dask.org/" target="_blank" rel="noopener"&gt;Dask&lt;/a&gt;, &lt;a href="https://ipyparallel.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;ipyparallel&lt;/a&gt;, &lt;a href="https://mpi4py.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;mpi4py&lt;/a&gt;, etc.)&lt;/li&gt;&#10;&lt;li&gt;Minimizes communication between scheduling layers&lt;/li&gt;&#10;&lt;li&gt;Handles job management through a simple database&lt;/li&gt;&#10;&lt;li&gt;Supports automatic fault tolerance and data saving&lt;/li&gt;&#10;&lt;li&gt;Works with any cluster scheduler (SLURM, PBS)&lt;/li&gt;&#10;&lt;li&gt;Provides real-time progress tracking in Jupyter notebooks&lt;/li&gt;&#10;&lt;li&gt;Scales to 50,000+ cores without breaking a sweat&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Used extensively for real quantum device simulations&lt;/li&gt;&#10;&lt;li&gt;26 GitHub stars (but don&amp;rsquo;t let that fool you - it&amp;rsquo;s highly specialized!)&lt;/li&gt;&#10;&lt;li&gt;Enables interactive supercomputing from a simple Jupyter notebook&lt;/li&gt;&#10;&lt;li&gt;Likely ran for many millions of core-hours across various projects&lt;/li&gt;&#10;&lt;li&gt;Actively maintained with 61 releases since 2018, proving its ongoing value&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-we-solved"&gt;🎯 Challenges We Solved &lt;a class="anchor" href="#-challenges-we-solved" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Scheduler overhead killing performance at scale&lt;/li&gt;&#10;&lt;li&gt;Data locality and fault tolerance&lt;/li&gt;&#10;&lt;li&gt;Real-time monitoring of massive parallel jobs&lt;/li&gt;&#10;&lt;li&gt;Making supercomputing accessible through notebooks&lt;/li&gt;&#10;&lt;li&gt;Automatic job recovery after crashes (e.g., when using spot VMs or Heisenbugs)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes you need to think outside the box&lt;/li&gt;&#10;&lt;li&gt;The best tools hide complexity behind simple interfaces&lt;/li&gt;&#10;&lt;li&gt;Real-time feedback is crucial, even for massive computations&lt;/li&gt;&#10;&lt;li&gt;When scaling up 1000x, you often need a completely different approach&lt;/li&gt;&#10;&lt;li&gt;Making things &amp;ldquo;just work&amp;rdquo; from a notebook is worth the effort&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to run massive parallel computations efficiently? Check out &lt;a href="https://github.com/basnijholt/adaptive-scheduler" target="_blank" rel="noopener"&gt;Adaptive Scheduler on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #HPC #QuantumComputing #ParallelComputing&lt;/p&gt;&#10;</description></item><item><title>Adaptive 📈</title><link>https://www.nijho.lt/post/advent-of-open-source/16-adaptive/</link><pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/16-adaptive/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;During my PhD, I was running quantum transport simulations on our 800-core computational cluster.&#10;Traditional parameter sweeps using dense grids were hitting the cluster&amp;rsquo;s limits - wasting precious compute time on uninteresting regions while potentially missing important features.&#10;Most parameter spaces we explored were &amp;ldquo;boring&amp;rdquo; with occasional interesting regions that required higher sampling density.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Initially, in 2016, I had a collection of one-off adaptive sampling scripts.&#10;Together with Joseph Weston and the support of my PhD advisor Anton Akhmerov, we turned it into a full-fledged Python package &amp;ldquo;&lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt;&amp;rdquo;.&#10;What began as an internal tool to make our cluster usage more efficient slowly gained traction in the wider scientific computing community.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Multiple adaptive sampling strategies (1D, 2D, ND, averaging stochastic functions)&lt;/li&gt;&#10;&lt;li&gt;Parallel execution support (local, MPI clusters, distributed systems)&lt;/li&gt;&#10;&lt;li&gt;Live plotting in Jupyter notebooks with real-time feedback&lt;/li&gt;&#10;&lt;li&gt;Customizable loss functions for different sampling priorities&lt;/li&gt;&#10;&lt;li&gt;Minimal dependencies&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact--applications"&gt;📊 Impact &amp;amp; Applications &lt;a class="anchor" href="#-impact--applications" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;1164 GitHub stars and thousands of daily downloads&lt;/li&gt;&#10;&lt;li&gt;Cited in 20+ scientific publications&lt;/li&gt;&#10;&lt;li&gt;Dependency of many packages, most notably: Orange Quantum System&amp;rsquo;s Quantify software for controlling real quantum computers!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Used in cutting-edge research across fields:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Quantum Computing&lt;/strong&gt;: Simulating and controlling superconducting qubits and trapped ion systems&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Quantum Physics&lt;/strong&gt;: Exploring topological quantum systems and novel materials&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Materials Science&lt;/strong&gt;: Studying graphene and other 2D materials&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Engineering&lt;/strong&gt;:&#10;&lt;ul&gt;&#10;&lt;li&gt;Optimizing radar system performance&lt;/li&gt;&#10;&lt;li&gt;Fluid dynamics simulations&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Chemistry&lt;/strong&gt;: Molecular modeling and vibrational spectroscopy&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Most Unexpected Use&lt;/strong&gt;: Found in a paper on predicting energy usage of wartime bungalows!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This range of applications shows how a tool built for quantum transport simulations can find surprising uses across disciplines, from fundamental physics to everyday buildings.&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Making parallel execution truly scalable while maintaining sampling intelligence&lt;/li&gt;&#10;&lt;li&gt;Creating intuitive visualization tools for complex parameter spaces&lt;/li&gt;&#10;&lt;li&gt;Handling stochastic functions and noisy data&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Building a user base takes active effort - giving talks and engaging online made all the difference&lt;/li&gt;&#10;&lt;li&gt;Good documentation converts interested users into regular contributors&lt;/li&gt;&#10;&lt;li&gt;This was my first open source package and learned all about packaging, testing, and CI/CD&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want smarter parameter space exploration? Check out &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;Adaptive on GitHub&lt;/a&gt; or try our &lt;a href="https://adaptive.readthedocs.io/en/latest/tutorial/tutorial.html" target="_blank" rel="noopener"&gt;interactive tutorial&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #ScientificComputing #ParallelComputing #Programming&lt;/p&gt;&#10;</description></item><item><title>clip-files 📋</title><link>https://www.nijho.lt/post/advent-of-open-source/15-clip-files/</link><pubDate>Sun, 15 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/15-clip-files/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the most useful tools are born from daily frustrations.&#10;When working with AI assistants like ChatGPT or Claude, I found myself constantly copying files to share context.&#10;After doing this manually dozens of times, I created a tool to automate it.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While contributing to open source, I frequently like to share entire (or large parts of) codebases with AI assistants for context or code review.&#10;Since the code is open source anyway, there&amp;rsquo;s no reason not to share the full context! But manually copying files, adding their paths for context, and ensuring I didn&amp;rsquo;t exceed token limits was tedious.&#10;Instead of continuing this manual process, I spent an evening creating &lt;a href="https://github.com/basnijholt/clip-files" target="_blank" rel="noopener"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt; - and have used it multiple times daily ever since.&lt;/p&gt;&#10;&lt;p&gt;In fact, I&amp;rsquo;m using it right now! The command (shown below) lets me share this entire advent calendar series with AI assistants for proofreading.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clip-files --initial-file ai.md --files introduction.md day*.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Uses &lt;code&gt;tiktoken&lt;/code&gt; for accurate GPT token counting&lt;/li&gt;&#10;&lt;li&gt;Supports both directory traversal and specific file selection&lt;/li&gt;&#10;&lt;li&gt;Automatically generates a file index for reference&lt;/li&gt;&#10;&lt;li&gt;Allows custom initial instructions via file&lt;/li&gt;&#10;&lt;li&gt;Preserves file paths in output for context&lt;/li&gt;&#10;&lt;li&gt;Clean, typed, and documented codebase&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While this tool has no GitHub stars (I haven&amp;rsquo;t promoted it), its real impact is personal - I use it multiple times daily for:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sharing entire open source codebases with AI assistants&lt;/li&gt;&#10;&lt;li&gt;Getting comprehensive code reviews&lt;/li&gt;&#10;&lt;li&gt;Discussing bug fixes with full context&lt;/li&gt;&#10;&lt;li&gt;Documentation improvements&lt;/li&gt;&#10;&lt;li&gt;Open source contributions&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The tool&amp;rsquo;s real value isn&amp;rsquo;t in its complexity (it&amp;rsquo;s just ~250 lines of code) but in how it removes friction from open source development.&#10;When working with open code, we might as well share all the context we can - and this tool makes that effortless.&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Maintaining consistent formatting across different input methods&lt;/li&gt;&#10;&lt;li&gt;Calculating accurate token counts for different components&lt;/li&gt;&#10;&lt;li&gt;Providing meaningful error messages for invalid inputs&lt;/li&gt;&#10;&lt;li&gt;Structuring output to be AI-assistant friendly&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;When working with open source code, sharing full context is valuable&lt;/li&gt;&#10;&lt;li&gt;Simple command-line tools can significantly improve daily workflows&lt;/li&gt;&#10;&lt;li&gt;Consistent formatting matters when working with AI assistants&lt;/li&gt;&#10;&lt;li&gt;Sometimes a 250-line tool can save hours of manual work&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to make sharing code with AI assistants easier? Check out &lt;a href="https://github.com/basnijholt/clip-files" target="_blank" rel="noopener"&gt;clip-files on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DevTools #AI #Programming&lt;/p&gt;&#10;</description></item><item><title>PhD Thesis Cover Design 🎨</title><link>https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/</link><pubDate>Sat, 14 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/14-phd-thesis-cover-design/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;When most people think of open source, they think of software. But what about open-sourcing your PhD thesis and creating a generative art system for its cover?&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With my PhD defense approaching and needing around 100 copies of my thesis, I faced a design challenge. As someone more comfortable with Python than Photoshop, I turned what started as a simple visualization idea into a full-blown generative art project with machine learning-based clustering.&lt;/p&gt;&#10;&lt;figure id="figure-multiple-printed-copies-of-the-thesis"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_8cd2dfb49d0239e5.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_91ed23b7c6d41771.webp 400w, https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_8cd2dfb49d0239e5.webp 760w, https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_1ad1cec7a7457f36.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="616" alt="A photograph of multiple printed copies of Bas&amp;#39;s PhD thesis laid out on a table outdoors. Each copy has a unique, colorful, generated cover design. Potted plants are visible in the background." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Multiple printed copies of the thesis&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Generated 3,671 unique covers from quantum physics simulation data&lt;/li&gt;&#10;&lt;li&gt;Used VGG16 neural network to extract features from covers&lt;/li&gt;&#10;&lt;li&gt;Implemented K-means clustering to group similar designs&lt;/li&gt;&#10;&lt;li&gt;Created interactive IPywidgets for design selection&lt;/li&gt;&#10;&lt;li&gt;Built a Binder app for real-time color scheme adjustment&lt;/li&gt;&#10;&lt;li&gt;Added unique edition numbers (1/120, 2/120, etc.)&lt;/li&gt;&#10;&lt;li&gt;Made both thesis and cover design code fully open source&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The project combines:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Adaptive sampling for physics simulations&lt;/li&gt;&#10;&lt;li&gt;Neural networks for image feature extraction&lt;/li&gt;&#10;&lt;li&gt;Interactive visualization tools&lt;/li&gt;&#10;&lt;li&gt;Parallel processing for batch generation&lt;/li&gt;&#10;&lt;li&gt;Version control for scientific writing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Thesis repository: 14 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Cover design repository: 36 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Real-world adoption: &lt;a href="https://repository.tudelft.nl/record/uuid:e88a1897-0033-47e3-8b4f-84fd9cd5eec0" target="_blank" rel="noopener"&gt;Dr. Tomas Pippia from TU Delft&lt;/a&gt; used the code to create his own thesis cover&lt;/li&gt;&#10;&lt;li&gt;~100 unique printed copies, each with its own generated cover and edition number&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Managing 3,671 unique PDF files and their conversions&lt;/li&gt;&#10;&lt;li&gt;Creating a clustering system for visually similar designs&lt;/li&gt;&#10;&lt;li&gt;Building interactive tools for non-technical collaborators&lt;/li&gt;&#10;&lt;li&gt;Ensuring consistent print quality across designs&lt;/li&gt;&#10;&lt;li&gt;Balancing artistic appeal with scientific relevance&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Open source isn&amp;rsquo;t just about code - it&amp;rsquo;s about sharing knowledge&lt;/li&gt;&#10;&lt;li&gt;Python can be a powerful tool for creative projects&lt;/li&gt;&#10;&lt;li&gt;Machine learning can help solve unexpected problems&lt;/li&gt;&#10;&lt;li&gt;Good visualization requires both technical skill and artistic sense&lt;/li&gt;&#10;&lt;li&gt;Interactive tools make collaboration with non-programmers easier&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="-fun-fact"&gt;📚 Fun Fact &lt;a class="anchor" href="#-fun-fact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Due to COVID restrictions, I defended my thesis without any guests present. The silver lining? I still have about 100 beautifully unique copies at home, each with a different generated cover and edition number. If you&amp;rsquo;d like a physical copy of this piece of quantum physics meets generative art, just let me know!&lt;/p&gt;&#10;&lt;p&gt;Want to create your own thesis cover or see how I wrote mine? Check out the &lt;a href="https://github.com/basnijholt/thesis-cover" target="_blank" rel="noopener"&gt;thesis cover project&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/thesis" target="_blank" rel="noopener"&gt;thesis source&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #PhD #Python #MachineLearning #DataVisualization&lt;/p&gt;&#10;</description></item><item><title>UniDep 🧬</title><link>https://www.nijho.lt/post/advent-of-open-source/13-unidep/</link><pubDate>Fri, 13 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/13-unidep/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever tried to set up a Python project that needs both pip and conda packages? Then you know the pain of maintaining multiple requirement files. &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;UniDep&lt;/a&gt; was born from years of frustration with this exact problem.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While working on large monorepos with hundreds of dependencies, I kept running into the same issue: maintaining separate files for conda (&lt;code&gt;environment.yml&lt;/code&gt;), pip (&lt;code&gt;requirements.txt&lt;/code&gt;), and Python packaging (&lt;code&gt;pyproject.toml&lt;/code&gt;). Coming from languages like Rust, where package management is a joy, Python&amp;rsquo;s fragmented ecosystem can feel overwhelming, especially to newcomers (obligatory &lt;a href="https://xkcd.com/1987/" target="_blank" rel="noopener"&gt;xkcd.com/1987&lt;/a&gt;).&lt;/p&gt;&#10;&lt;p&gt;What started as a simple tool to avoid duplicate dependency definitions turned into something much more powerful. In enterprise environments, it&amp;rsquo;s common to find monorepos with over 1000 dependencies spread across dozens of packages. Setting up these environments traditionally involves following lengthy setup guides, installing packages in the correct order, and hoping nothing breaks.&lt;/p&gt;&#10;&lt;p&gt;Then came the &amp;ldquo;aha&amp;rdquo; moment - what if we could install everything with a single command? After implementing UniDep, those complex setup guides became a one-liner: &lt;code&gt;unidep install-all -e&lt;/code&gt;. It installs all dependencies and sets up every package in the monorepo in editable mode. What used to take hours now takes minutes.&lt;/p&gt;&#10;&lt;figure id="figure-the-difficulty-of-managing-pip-and-conda"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_1916102bc051ce0e.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_992ea341768024aa.webp 400w, https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_1916102bc051ce0e.webp 760w, https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_283eb791a4b7e11e.webp 980w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="447" alt="Meme with the text &amp;#39;ONE DOES NOT SIMPLY MANAGE BOTH PIP AND CONDA DEPENDENCIES&amp;#39; featuring Sean Bean as Boromir from The Lord of the Rings." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The difficulty of managing pip and conda&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Single source of truth for all dependencies&lt;/li&gt;&#10;&lt;li&gt;Supports both &lt;code&gt;requirements.yaml&lt;/code&gt; and &lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Handles platform-specific dependencies&lt;/li&gt;&#10;&lt;li&gt;Generates conda-lock files both for entire monorepos and fully consistent lock files for its individual packages&lt;/li&gt;&#10;&lt;li&gt;Works with pip, conda, and mamba&lt;/li&gt;&#10;&lt;li&gt;Monorepo-friendly with local dependency support&lt;/li&gt;&#10;&lt;li&gt;Integrates with setuptools and hatchling&lt;/li&gt;&#10;&lt;li&gt;≥99% test coverage, fully-typed&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;221 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Featured on &lt;a href="https://www.youtube.com/live/PRaTs3PnJvI?si=UrVozo81Pj8WcyXh&amp;amp;t=489" target="_blank" rel="noopener"&gt;Python Bytes podcast&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Saves countless hours of dependency management&lt;/li&gt;&#10;&lt;li&gt;Makes complex project setups accessible&lt;/li&gt;&#10;&lt;li&gt;Actively maintained with 70 releases&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Different package naming between pip and conda&lt;/li&gt;&#10;&lt;li&gt;Platform-specific dependencies&lt;/li&gt;&#10;&lt;li&gt;Cross-platform compatibility (Linux, Windows, MacOS)&lt;/li&gt;&#10;&lt;li&gt;Supporting pip-only workflows&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Good developer experience is worth the effort&lt;/li&gt;&#10;&lt;li&gt;Sometimes you need to build a bridge between existing tools&lt;/li&gt;&#10;&lt;li&gt;The best tools make hard things easy&lt;/li&gt;&#10;&lt;li&gt;Dependency resolution is hard but can be simple for users&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="-future-plans"&gt;🔮 Future Plans &lt;a class="anchor" href="#-future-plans" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Adding Pixi support - allowing seamless migration to Pixi without changing config files&lt;/li&gt;&#10;&lt;li&gt;Eventually, Pixi might make UniDep obsolete (and that&amp;rsquo;s okay!)&lt;/li&gt;&#10;&lt;li&gt;For now, UniDep bridges the gap between pip and conda, making life easier for developers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want simpler dependency management? Check out &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;UniDep on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DevTools #Programming #PackageManagement #Conda&lt;/p&gt;&#10;</description></item><item><title>instacron 📸</title><link>https://www.nijho.lt/post/advent-of-open-source/12-instacron/</link><pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/12-instacron/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever found yourself with beautiful photos but too lazy to post them? Instead of changing my habits, I did what any developer would do: spent way more time automating the process than it would have taken to post manually.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2017, I had hundreds of landscape photos from traveling through South America that I wanted to share on Instagram. The solution? Write a Python script that would randomly select photos and post them automatically, complete with philosophical quotes randomly selected from Bukowski, Einstein, or Dostoevsky because&amp;hellip; it seemed to be the default of all &amp;ldquo;influencers&amp;rdquo; at the time. Nothing says &amp;ldquo;authentic travel experience&amp;rdquo; like an auto-generated Bukowski quote about drinking next to your serene mountain photo!&lt;/p&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_b99c73d57edbea5b.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_5a3939660236b043.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_b99c73d57edbea5b.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_749d311a92e20dbc.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="433" alt="Screenshot of an Instagram post with a person standing in a stone archway overlooking a sunset, captioned &amp;#39;Taken in United States, Santa Barbara&amp;#39; with hashtags related to nature and travel." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_847939577f1e3dd1.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_a10eeae7bee69925.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_847939577f1e3dd1.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_12c1ed0c7f55a937.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="558" alt="Screenshot of an Instagram post of a cave with a river flowing through it, captioned &amp;#39;Taken in Thailand, some cave&amp;#39; with hashtags related to travel and adventure." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_8832527ce6759925.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_5430615296d94efb.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_8832527ce6759925.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_be87e46e6482de14.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="473" alt="Screenshot of an Instagram post showing a misty mountain landscape at sunrise, captioned &amp;#39;Taken in Thailand-Laos border&amp;#39; with hashtags related to nature and travel." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_10c2294abc4f0ed4.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_8e63ad58ac7e0b76.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_10c2294abc4f0ed4.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_226dfc0f12041227.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="480" alt="Screenshot of an Instagram post of a mountain range with a green field in the foreground, captioned &amp;#39;Taken in Lago Argentino, SC, Argentina&amp;#39; with hashtags related to nature and travel." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Randomly selects photos from a directory&lt;/li&gt;&#10;&lt;li&gt;Adds location data from EXIF metadata&lt;/li&gt;&#10;&lt;li&gt;Generates captions with random philosophical quotes&lt;/li&gt;&#10;&lt;li&gt;Adds two random emojis to match the &amp;ldquo;mood&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Automatically generates relevant hashtags&lt;/li&gt;&#10;&lt;li&gt;Tracks posting frequency&lt;/li&gt;&#10;&lt;li&gt;Uses cron for scheduled posting&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;21 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;4143 Instagram followers&lt;/li&gt;&#10;&lt;li&gt;Successfully posted hundreds of photos&lt;/li&gt;&#10;&lt;li&gt;Generated some concerned DMs (more on that below!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Instagram&amp;rsquo;s ever-changing API&lt;/li&gt;&#10;&lt;li&gt;Handling rate limits and restrictions&lt;/li&gt;&#10;&lt;li&gt;Ensuring photos weren&amp;rsquo;t posted multiple times&lt;/li&gt;&#10;&lt;li&gt;Making captions interesting without human input&lt;/li&gt;&#10;&lt;li&gt;The delicate art of random emoji selection&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-funny-story"&gt;😅 Funny Story &lt;a class="anchor" href="#-funny-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The random quote and emoji selector once created an unexpectedly dramatic post. Picture this: a beautiful landscape photo paired with a deeply philosophical quote about solitude, and topped with a broken heart emoji (💔). Within hours, I started receiving concerned DMs from friends asking if everything was okay and whether my relationship had ended!&lt;/p&gt;&#10;&lt;p&gt;I had to explain that no, I wasn&amp;rsquo;t going through a breakup - my bot just had a flair for the dramatic! These occasional mismatches made the whole thing more entertaining.&lt;/p&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes the best projects come from pure laziness&lt;/li&gt;&#10;&lt;li&gt;Automation doesn&amp;rsquo;t always save time, but it&amp;rsquo;s fun&lt;/li&gt;&#10;&lt;li&gt;Never underestimate the power of a misplaced emoji to trigger concerned messages&lt;/li&gt;&#10;&lt;li&gt;Some followers actually care about you (even if they&amp;rsquo;re worried about your bot-generated content)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;While I haven&amp;rsquo;t used this project in 6 years (Instagram&amp;rsquo;s API changes eventually won), it remains a testament to the lengths we&amp;rsquo;ll go to avoid repetitive tasks. Check out &lt;a href="https://github.com/basnijholt/instacron" target="_blank" rel="noopener"&gt;instacron on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Automation #Photography #Instagram&lt;/p&gt;&#10;</description></item><item><title>conda-forge Contributions 🐍</title><link>https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/</link><pubDate>Wed, 11 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/11-conda-forge-contributions/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Unlike other projects in this advent calendar, I want to celebrate being part of something much bigger: the &lt;a href="https://conda-forge.org/" target="_blank" rel="noopener"&gt;conda-forge&lt;/a&gt; community, where thousands of volunteers maintain the packages that power scientific Python.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I started with conda-forge in 2016 during my PhD work with Kwant, a quantum transport simulator. Installing it was a nightmare - it required system-level numerical libraries like MUMPS and Scotch which required a special config file to link. When I discovered conda-forge and its mission to make scientific software installation painless, I knew this was the solution!&lt;/p&gt;&#10;&lt;figure id="figure-the-humble-work-of-conda-forge-maintainers"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_227391ecf0bd252.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_9b170f53ebe4dc1b.webp 400w, https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_227391ecf0bd252.webp 760w, https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_95b0c28d6df1a41a.webp 978w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="586" alt="Meme showing a farmer standing in a field, with the text &amp;#39;MAINTAINING CONDA-FORGE PACKAGES FOR THE SCIENTIFIC COMMUNITY&amp;#39; above and &amp;#39;It ain&amp;#39;t much, but it&amp;#39;s honest work&amp;#39; below." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The humble work of conda-forge maintainers&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over the past 8 years, I&amp;rsquo;ve maintained over 40 build recipes, including:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Major scientific packages like &lt;a href="https://vtk.org/" target="_blank" rel="noopener"&gt;VTK&lt;/a&gt;, &lt;a href="https://mumps-solver.org/" target="_blank" rel="noopener"&gt;MUMPS&lt;/a&gt;, and &lt;a href="https://www.labri.fr/perso/pelegrin/scotch/" target="_blank" rel="noopener"&gt;Scotch&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Quantum physics tools like &lt;a href="https://quantumai.google/qsim" target="_blank" rel="noopener"&gt;Qsim&lt;/a&gt;, &lt;a href="https://github.com/microsoft/Qcodes" target="_blank" rel="noopener"&gt;Qcodes&lt;/a&gt;, and &lt;a href="https://kwant-project.org/" target="_blank" rel="noopener"&gt;Kwant&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Visualization libraries like &lt;a href="https://holoviews.org/" target="_blank" rel="noopener"&gt;HoloViews&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Plus all the packages I&amp;rsquo;ve authored myself&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The real magic of conda-forge is its infrastructure:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Automated builds across Linux, macOS, and Windows&lt;/li&gt;&#10;&lt;li&gt;Strict dependency version management&lt;/li&gt;&#10;&lt;li&gt;Comprehensive CI/CD pipelines&lt;/li&gt;&#10;&lt;li&gt;Community-driven quality control&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What makes conda-forge truly special is how initial contributions blossom into collaborative efforts. I&amp;rsquo;ve experienced this firsthand: after creating the initial MUMPS feedstock, 25 other contributors joined in, each bringing their unique expertise.&lt;/p&gt;&#10;&lt;p&gt;A recent experience also illustrates this: I added CUDA support to both &lt;a href="https://github.com/conda-forge/qsimcirq-feedstock/pull/3" target="_blank" rel="noopener"&gt;Google&amp;rsquo;s Qsim&lt;/a&gt; and &lt;a href="https://github.com/conda-forge/qiskit-aer-feedstock/pull/19" target="_blank" rel="noopener"&gt;IBM&amp;rsquo;s Cirq&lt;/a&gt; packages. Shortly after, &lt;a href="https://github.com/leofang" target="_blank" rel="noopener"&gt;Leo Fang&lt;/a&gt;, an engineer from NVIDIA who knows CUDA far better than I do, stepped up and made significant improvements. This is the beauty of open source - experts naturally gravitate to where they can make the biggest impact.&lt;/p&gt;&#10;&lt;p&gt;This collaborative approach solves a problem I&amp;rsquo;ve encountered at several companies: the tendency to maintain complicated build systems internally with custom hacks. Instead of each organization reinventing the wheel, conda-forge provides a shared platform where:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Maintenance burden is distributed&lt;/li&gt;&#10;&lt;li&gt;Expert knowledge flows freely&lt;/li&gt;&#10;&lt;li&gt;Build practices are standardized&lt;/li&gt;&#10;&lt;li&gt;Infrastructure is public and reusable&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Keeping up with upstream changes&lt;/li&gt;&#10;&lt;li&gt;Managing complex dependency trees&lt;/li&gt;&#10;&lt;li&gt;Cross-platform compatibility&lt;/li&gt;&#10;&lt;li&gt;Coordinating with upstream maintainers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Community effort beats solo work for infrastructure&lt;/li&gt;&#10;&lt;li&gt;Automation is crucial for reliability&lt;/li&gt;&#10;&lt;li&gt;Small contributions compound over time&lt;/li&gt;&#10;&lt;li&gt;Being part of something bigger is rewarding&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to contribute to scientific Python? Check out &lt;a href="https://conda-forge.org/" target="_blank" rel="noopener"&gt;conda-forge&lt;/a&gt; and the &lt;a href="https://github.com/conda-forge/staged-recipes" target="_blank" rel="noopener"&gt;staged-recipes repository&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Scientific #Programming #Community&lt;/p&gt;&#10;</description></item><item><title>Home Assistant Config 🏠</title><link>https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/</link><pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/10-home-assistant-config/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Funny how things work out - initially, I didn&amp;rsquo;t care for home automation at all. But when in 2020 I discovered Home Assistant was written in Python, my favorite programming language, I suddenly found a new hobby. Over the years, I&amp;rsquo;ve built a comprehensive setup with over 137 automations (and counting!). These automations are triggered by everything from simple motion sensors to complex patterns of conditions and sensor states, creating a home that responds intelligently to our needs.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What started with basic light controls evolved into a complete home automation system. In the beginning, my wife was continuously annoyed by lights turning on at the wrong moment or not at all. However, as I debugged and refined the system, things started working exactly as expected, and she began to really appreciate it. When we moved from The Netherlands to the US in 2022 (and two more times since), we felt like we were back in the stone age having to manually control lights. The annoyance had shifted from buggy automations to not having Home Assistant fully installed yet!&lt;/p&gt;&#10;&lt;p&gt;Unfortunately, I&amp;rsquo;ve been less active lately with my Home Assistant setup because so many other projects draw my attention. One of these &amp;ldquo;projects&amp;rdquo; is my 1.5-year-old twin boys, who now have their own room with specialized automations, including 100% local cameras with ML pattern detection for monitoring without compromising privacy.&lt;/p&gt;&#10;&lt;figure id="figure-the-evolution-of-home-automation"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_6700885751a5eed.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_a2f1eaea094dec73.webp 285w, https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_6700885751a5eed.webp 542w, https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_2a8639bb88253b09.webp 856w" sizes="auto, (max-width: 760px) 100vw, 720px" width="542" height="760" alt="Meme with four panels, each showing a brain with increasing complexity. The panels are labeled: &amp;#39;MANUAL LIGHT SWITCHES&amp;#39;, &amp;#39;SMART BULBS&amp;#39;, &amp;#39;MOTION SENSORS&amp;#39;, and &amp;#39;137 AUTOMATIONS RUNNING YOUR ENTIRE HOME&amp;#39;." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The evolution of home automation&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;137 documented automations with auto-generated documentation&lt;/li&gt;&#10;&lt;li&gt;Presence detection in every room&lt;/li&gt;&#10;&lt;li&gt;Adaptive lighting throughout the house&lt;/li&gt;&#10;&lt;li&gt;Privacy-focused baby monitoring&lt;/li&gt;&#10;&lt;li&gt;Fully local processing&lt;/li&gt;&#10;&lt;li&gt;2,432 commits of continuous improvement&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;1,747 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;185 forks&lt;/li&gt;&#10;&lt;li&gt;Used as reference by countless Home Assistant enthusiasts&lt;/li&gt;&#10;&lt;li&gt;Featured in Home Assistant community&lt;/li&gt;&#10;&lt;li&gt;Spawned several spin-off projects (like &lt;a href="../02-adaptive-lighting"&gt;Adaptive Lighting&lt;/a&gt; from Day 2!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Start small, expand gradually&lt;/li&gt;&#10;&lt;li&gt;Privacy and local processing matter&lt;/li&gt;&#10;&lt;li&gt;Automated documentation stays accurate&lt;/li&gt;&#10;&lt;li&gt;Home automation is never &amp;ldquo;finished&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Sometimes life (like twins!) brings new automation challenges&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to explore my home automation setup? Check out &lt;a href="https://github.com/basnijholt/home-assistant-config" target="_blank" rel="noopener"&gt;the full config on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #SmartHome #IoT #Python&lt;/p&gt;&#10;</description></item><item><title>yaml2bib 📚</title><link>https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/</link><pubDate>Mon, 09 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/09-yaml2bib/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Every academic writing in LaTeX knows the pain: managing BibTeX files with hundreds of citations, each potentially formatted slightly differently, with inconsistent journal abbreviations, and duplicated entries across chapters. While procrastinating on writing my PhD thesis, I became obsessed with solving this universal academic headache.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When writing a PhD thesis with multiple chapters that share citations, maintaining consistency is crucial but tedious. Different chapters might cite the same paper but with slightly different BibTeX entries - one using the full journal name, another using abbreviations, or worse, having slightly different titles or author lists. Rather than manually standardizing ~350 citations, I created yaml2bib: give it a list of DOIs in YAML format, and it generates a perfect BibTeX file with consistent journal abbreviations and formatting.&lt;/p&gt;&#10;&lt;figure id="figure-the-pain-of-manual-bibtex-management"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_aac0680d796f5d9.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_f547b35ecc31869c.webp 400w, https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_aac0680d796f5d9.webp 760w, https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_5f55d375b7929c53.webp 976w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="371" alt="Meme depicting a cartoon dog sitting calmly in a room engulfed in flames, saying &amp;#39;This is fine.&amp;#39; The dog is labeled &amp;#39;Managing 300&amp;#43; BibTeX entries manually&amp;#39;." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The pain of manual BibTeX management&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Fetches citation data from CrossRef using DOIs&lt;/li&gt;&#10;&lt;li&gt;Ensures consistent journal name abbreviations&lt;/li&gt;&#10;&lt;li&gt;Caches API responses to avoid repeated queries&lt;/li&gt;&#10;&lt;li&gt;Supports custom text replacements&lt;/li&gt;&#10;&lt;li&gt;Combines multiple YAML sources&lt;/li&gt;&#10;&lt;li&gt;Works as both CLI tool and Python library&lt;/li&gt;&#10;&lt;li&gt;Zero manual citation formatting needed&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While this tool has only 11 GitHub stars, it saved me countless hours during thesis writing and helped ensure professional consistency in my citations. It&amp;rsquo;s particularly useful for:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Large academic documents with many citations&lt;/li&gt;&#10;&lt;li&gt;Multi-chapter works sharing references&lt;/li&gt;&#10;&lt;li&gt;Collaborative papers needing citation standardization&lt;/li&gt;&#10;&lt;li&gt;Anyone tired of manually formatting BibTeX entries&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Procrastination can be productive if channeled right (story of my life&amp;hellip;)&lt;/li&gt;&#10;&lt;li&gt;A single source of truth prevents inconsistencies&lt;/li&gt;&#10;&lt;li&gt;Good tools make you care about details you&amp;rsquo;d otherwise skip&lt;/li&gt;&#10;&lt;li&gt;Every LaTeX user has fought with BibTeX at some point&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want perfectly consistent citations? Check out &lt;a href="https://github.com/basnijholt/yaml2bib" target="_blank" rel="noopener"&gt;yaml2bib on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Academia #LaTeX #Research&lt;/p&gt;&#10;</description></item><item><title>Home Assistant Stream Deck YAML 🎮</title><link>https://www.nijho.lt/post/advent-of-open-source/08-home-assistant-stream-deck-yaml/</link><pubDate>Sun, 08 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/08-home-assistant-stream-deck-yaml/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;What started as a personal hack to control my home (temperature, lights, TV, music) with a gifted Stream Deck evolved into something bigger. This project became my first real exploration of AI-assisted development, showing me how much faster I could work with AI as a tool. With 250+ stars and 9 contributors adding significant features, it&amp;rsquo;s grown beyond my initial vision.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While the Stream Deck&amp;rsquo;s official software is great for streamers, it doesn&amp;rsquo;t support Linux and its Home Assistant integration is not very programmable. After creating a simple solution using &lt;a href="https://github.com/abcminiuser/python-elgato-streamdeck" target="_blank" rel="noopener"&gt;&lt;code&gt;python-elgato-streamdeck&lt;/code&gt;&lt;/a&gt;, I realized others might be interested. Using AI as a development partner changed my workflow - it helped write tests, reviewed code, and generated use cases I hadn&amp;rsquo;t thought of. What really surprised me was asking AI to brainstorm example configurations - it generated over 30 creative use cases with complete code examples, which I then turned into unit tests. These tests now automatically generate the documentation using &lt;a href="https://github.com/basnijholt/markdown-code-runner" target="_blank" rel="noopener"&gt;&lt;code&gt;markdown-code-runner&lt;/code&gt; &lt;/a&gt;(&lt;a href="../05-markdown-code-runner"&gt;from Day 5&lt;/a&gt;).&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;YAML configuration with Jinja2 templating&lt;/li&gt;&#10;&lt;li&gt;Supports all Stream Deck models, including the Plus with dials&lt;/li&gt;&#10;&lt;li&gt;Cross-platform (Linux, macOS, Windows)&lt;/li&gt;&#10;&lt;li&gt;30+ example configurations (AI-generated, then human-refined)&lt;/li&gt;&#10;&lt;li&gt;Documentation auto-generated from code and unit tests&lt;/li&gt;&#10;&lt;li&gt;Available as a Home Assistant add-on&lt;/li&gt;&#10;&lt;li&gt;Community-contributed features like touchscreen support and dial controls&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;AI can significantly speed up development&lt;/li&gt;&#10;&lt;li&gt;Faster development enables broader project scope&lt;/li&gt;&#10;&lt;li&gt;Good examples make the best documentation&lt;/li&gt;&#10;&lt;li&gt;Tools building upon tools multiply productivity&lt;/li&gt;&#10;&lt;li&gt;Community contributions can take a project in exciting new directions&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to control Home Assistant with a Stream Deck? Check out &lt;a href="https://github.com/basnijholt/home-assistant-streamdeck-yaml" target="_blank" rel="noopener"&gt;the project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #SmartHome #Python #AI&lt;/p&gt;&#10;</description></item><item><title>numthreads 🧵</title><link>https://www.nijho.lt/post/advent-of-open-source/07-numthreads/</link><pubDate>Sat, 07 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/07-numthreads/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the smallest tools solve the most persistent problems.&#10;Today&amp;rsquo;s project is about taming automatic parallelization in scientific computing - a deceptively simple challenge that has cost countless CPU hours.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While working on high-performance computing clusters, I frequently encountered a counterintuitive problem: code running slower on multiple CPU cores than on a single one.&#10;Many scientific libraries (NumPy, SciPy) automatically parallelize operations, which sounds great but can actually harm performance when you&amp;rsquo;re already parallelizing at a higher level.&#10;After repeatedly explaining to colleagues why they needed to set various environment variables to disable this behavior, I created &lt;a href="https://github.com/basnijholt/numthreads" target="_blank" rel="noopener"&gt;&lt;code&gt;numthreads&lt;/code&gt;&lt;/a&gt; to solve it once and for all.&lt;/p&gt;&#10;&lt;figure id="figure-overenthusiastic-threading"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_4d36ab41a66a48cb.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_8178332437a0cb36.webp 297w, https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_4d36ab41a66a48cb.webp 565w, https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_f55150e31c7bd3b9.webp 891w" sizes="auto, (max-width: 760px) 100vw, 720px" width="565" height="760" alt="Meme showing a person labeled &amp;#39;YOUR CODE&amp;#39; about to be hugged by a pink creature labeled &amp;#39;NUMTHREADS.SET(1)&amp;#39;, representing setting the number of threads to 1. In the background, a yellow circle labeled &amp;#39;CPU PERFORMANCE&amp;#39; is visible." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Overenthusiastic threading&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Controls thread count for major numerical libraries:&#10;&lt;ul&gt;&#10;&lt;li&gt;OpenBLAS&lt;/li&gt;&#10;&lt;li&gt;Intel&amp;rsquo;s Math Kernel Library (MKL)&lt;/li&gt;&#10;&lt;li&gt;OpenMP&lt;/li&gt;&#10;&lt;li&gt;NumExpr&lt;/li&gt;&#10;&lt;li&gt;Accelerate&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies&lt;/li&gt;&#10;&lt;li&gt;Tiny (≤7KB) yet solves a real problem&lt;/li&gt;&#10;&lt;li&gt;Both CLI and Python API&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over my scientific computing career, I&amp;rsquo;ve encountered this threading issue dozens of times. Since creating this package a year ago, I&amp;rsquo;ve helped at least 5 different researchers speed up their workflows, sometimes by orders of magnitude. The beauty lies in its simplicity - the fix is often as simple as:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;numthreads 1&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The simplest solutions are often the best&lt;/li&gt;&#10;&lt;li&gt;More cores doesn&amp;rsquo;t always mean faster code&lt;/li&gt;&#10;&lt;li&gt;If you find yourself repeating the same instructions, write a tool&lt;/li&gt;&#10;&lt;li&gt;Even a 7KB package can save hours of computing time&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want better control over your numerical computations? Check out &lt;a href="https://github.com/basnijholt/numthreads" target="_blank" rel="noopener"&gt;numthreads on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #HPC #ScientificComputing #Programming&lt;/p&gt;&#10;</description></item><item><title>Open Science Publications 🔬</title><link>https://www.nijho.lt/post/advent-of-open-source/06-open-science-publications/</link><pubDate>Fri, 06 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/06-open-science-publications/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Today&amp;rsquo;s post is about something I have strong opinions on: making scientific research reproducible. While many researchers talk about open science, actually making your work reproducible for anyone requires significant effort.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;During my Ph.D. and subsequent research, I noticed a frustrating pattern in scientific publications: claims of &amp;ldquo;code available upon request&amp;rdquo; that often led nowhere, or code snippets that wouldn&amp;rsquo;t run without substantial modification. I made an effort that my publications would be different – each includes complete, runnable code that reproduces every figure and result.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Multiple repositories covering quantum physics experiments and simulations:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/orbitalfield" target="_blank" rel="noopener"&gt;orbitalfield&lt;/a&gt; - &amp;ldquo;Orbital effect of magnetic field on the Majorana phase diagram&amp;rdquo; (94 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/supercurrent-majorana-nanowire" target="_blank" rel="noopener"&gt;supercurrent-majorana-nanowire&lt;/a&gt; - &amp;ldquo;Supercurrent interference in few-mode nanowire Josephson junctions&amp;rdquo; (78 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/zigzag-majoranas" target="_blank" rel="noopener"&gt;zigzag-majoranas&lt;/a&gt; - &amp;ldquo;Enhanced proximity effect in zigzag-shaped Majorana Josephson junctions&amp;rdquo; (56 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/microsoft/azure-quantum-tgp" target="_blank" rel="noopener"&gt;azure-quantum-tgp&lt;/a&gt; - &amp;ldquo;Protocol to identify a topological superconducting phase in a three-terminal device&amp;rdquo; (52 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/spin-orbit-nanowires" target="_blank" rel="noopener"&gt;spin-orbit-nanowires&lt;/a&gt; - &amp;ldquo;Spin-orbit protection of induced superconductivity in Majorana nanowires&amp;rdquo; (81 citations)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-evolution-of-best-practices"&gt;🔄 Evolution of Best Practices &lt;a class="anchor" href="#-evolution-of-best-practices" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What I consider best practices has evolved over time. Today, I would:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Use &lt;a href="https://pixi.sh/" target="_blank" rel="noopener"&gt;Pixi&lt;/a&gt; for universal lock files across operating systems and programming languages&lt;/li&gt;&#10;&lt;li&gt;Provide a minimal Docker container that just installs the pixi lock file&lt;/li&gt;&#10;&lt;li&gt;Create self-documenting Jupyter notebooks that reproduce every result&lt;/li&gt;&#10;&lt;li&gt;Include clear figure-to-code mapping for paper reproducibility&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Balancing code cleanliness with research deadlines&lt;/li&gt;&#10;&lt;li&gt;Managing large datasets efficiently&lt;/li&gt;&#10;&lt;li&gt;Ensuring long-term reproducibility&lt;/li&gt;&#10;&lt;li&gt;Making complex physics simulations accessible&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Clean code takes time but saves more in the long run&lt;/li&gt;&#10;&lt;li&gt;Documentation is as crucial as the code itself&lt;/li&gt;&#10;&lt;li&gt;Lock files are essential for true reproducibility&lt;/li&gt;&#10;&lt;li&gt;Making code public improves its quality&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to explore quantum physics simulations? Check out the repositories above, each linked to their corresponding papers with full reproduction instructions.&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #OpenScience #Physics #QuantumComputing #Python&lt;/p&gt;&#10;</description></item><item><title>markdown-code-runner 📝</title><link>https://www.nijho.lt/post/advent-of-open-source/05-markdown-code-runner/</link><pubDate>Thu, 05 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/05-markdown-code-runner/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Documentation should be as close to the code as possible - that&amp;rsquo;s the principle of &amp;ldquo;information locality.&amp;rdquo; But how do you keep code examples and their outputs in sync? Enter markdown-code-runner, a tool that executes code blocks within Markdown files and updates their output in-place!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While maintaining various open source and work projects, I found myself constantly updating README or Wiki files with new command outputs, updated tables, and fresh examples. This manual process was error-prone and tedious. Instead of using external templating files or more advanced documentation generators like Sphinx and Quarto (which are often overkill for single-page projects), I wanted something simpler: code and its output living together in the same Markdown file, always in sync.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Executes code blocks (Python and Bash) within Markdown files&lt;/li&gt;&#10;&lt;li&gt;Updates outputs in-place while preserving the original file structure&lt;/li&gt;&#10;&lt;li&gt;Supports hidden code blocks for generating content without showing the code&lt;/li&gt;&#10;&lt;li&gt;Works with any language by using file code blocks and bash execution&lt;/li&gt;&#10;&lt;li&gt;Zero external dependencies&lt;/li&gt;&#10;&lt;li&gt;GitHub Actions integration for automatic README updates&lt;/li&gt;&#10;&lt;li&gt;Preserves Markdown spec compatibility by hiding code in HTML comments (&lt;code&gt;&amp;lt;!-- CODE HERE --&amp;gt;&lt;/code&gt;)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;97 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Used in many of my other projects for auto-generating:&#10;&lt;ul&gt;&#10;&lt;li&gt;Command-line help texts&lt;/li&gt;&#10;&lt;li&gt;API documentation&lt;/li&gt;&#10;&lt;li&gt;Example outputs&lt;/li&gt;&#10;&lt;li&gt;Data tables&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Helps maintain &amp;ldquo;information locality&amp;rdquo; - everything needed is in one file&lt;/li&gt;&#10;&lt;li&gt;Enables dynamic documentation that stays up-to-date automatically&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Parsing Markdown without breaking its structure&lt;/li&gt;&#10;&lt;li&gt;Supporting multiple programming languages&lt;/li&gt;&#10;&lt;li&gt;Making it work seamlessly with GitHub Actions&lt;/li&gt;&#10;&lt;li&gt;Handling complex outputs (tables, images, etc.)&lt;/li&gt;&#10;&lt;li&gt;Maintaining backward compatibility with existing Markdown files&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Documentation is best when it&amp;rsquo;s close to what it documents&lt;/li&gt;&#10;&lt;li&gt;Automation should be invisible - the tool shouldn&amp;rsquo;t change how you write Markdown&lt;/li&gt;&#10;&lt;li&gt;Sometimes the simplest solution (using HTML comments) is the best&lt;/li&gt;&#10;&lt;li&gt;Good documentation tools should make maintaining docs easier, not harder&lt;/li&gt;&#10;&lt;li&gt;Auto-generated content should be clearly marked as such&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to keep your documentation always in sync? Check out &lt;a href="https://github.com/basnijholt/markdown-code-runner" target="_blank" rel="noopener"&gt;markdown-code-runner on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Documentation #DevTools #Programming&lt;/p&gt;&#10;</description></item><item><title>WenFire: When Could I retire?! 🔥</title><link>https://www.nijho.lt/post/advent-of-open-source/04-wenfire/</link><pubDate>Wed, 04 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/04-wenfire/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;While I absolutely love my job, I believe I&amp;rsquo;ll love it even more when my family&amp;rsquo;s livelihood doesn&amp;rsquo;t depend on it.&#10;That&amp;rsquo;s the dream of financial independence - doing what you love because you want to, not because you have to.&#10;This led me to create &lt;a href="https://wenfire.nijho.lt/" target="_blank" rel="noopener"&gt;WenFire&lt;/a&gt;, a financial independence calculator that helps answer the eternal questions: &amp;ldquo;When can I retire?&amp;rdquo; and &amp;ldquo;How many days of extra work will that new iPhone or car cost me?&amp;rdquo;&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;There are many retirement calculators out there, but they all made assumptions I didn&amp;rsquo;t quite agree with (and I&amp;rsquo;m sure others won&amp;rsquo;t agree with my assumptions either!).&#10;I wanted something that matched my specific views on salary growth and investment returns.&#10;Plus, I had been itching to learn more about building web applications, so this project let me explore FastAPI, htmx, and Vega-Lite while solving a personal need.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Built with FastAPI and Bootstrap&lt;/li&gt;&#10;&lt;li&gt;Interactive visualizations using Vega-Lite&lt;/li&gt;&#10;&lt;li&gt;Responsive and mobile-friendly design&lt;/li&gt;&#10;&lt;li&gt;Customizable assumptions for:&#10;&lt;ul&gt;&#10;&lt;li&gt;Investment growth&lt;/li&gt;&#10;&lt;li&gt;Inflation rates&lt;/li&gt;&#10;&lt;li&gt;Salary increases&lt;/li&gt;&#10;&lt;li&gt;Safe withdrawal rates&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Allows parameter changes at certain dates&lt;/li&gt;&#10;&lt;li&gt;Calculate retirement impact of one-time expenses&lt;/li&gt;&#10;&lt;li&gt;All parameters are encoded in the URL for easy sharing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While this project only has 3 GitHub stars, its real value is personal - it helps me track my own journey to financial independence.&#10;It&amp;rsquo;s also been a great learning experience in web development, an area where I had little prior experience.&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Learning web development from scratch&lt;/li&gt;&#10;&lt;li&gt;Creating intuitive visualizations for financial data&lt;/li&gt;&#10;&lt;li&gt;Balancing simplicity with customization options&lt;/li&gt;&#10;&lt;li&gt;Making reasonable default assumptions&lt;/li&gt;&#10;&lt;li&gt;Ensuring mobile responsiveness&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Financial modeling requires careful consideration of assumptions&lt;/li&gt;&#10;&lt;li&gt;Web development is a different beast from scientific computing&lt;/li&gt;&#10;&lt;li&gt;Sometimes the best way to learn new tech is to solve a personal problem&lt;/li&gt;&#10;&lt;li&gt;The best calculator is the one that matches your own assumptions&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to calculate your path to financial independence? Try it at &lt;a href="https://wenfire.nijho.lt/" target="_blank" rel="noopener"&gt;wenfire.nijho.lt&lt;/a&gt; or check out the &lt;a href="https://github.com/basnijholt/wenfire" target="_blank" rel="noopener"&gt;source code&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #FinancialIndependence #WebDev #FIRE&lt;/p&gt;&#10;</description></item><item><title>rsync-time-machine.py 🕒</title><link>https://www.nijho.lt/post/advent-of-open-source/03-rsync-time-machine/</link><pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/03-rsync-time-machine/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the best open source projects start with &amp;ldquo;I can do better than this.&amp;rdquo; That&amp;rsquo;s exactly what happened when I looked at &lt;a href="https://github.com/laurent22/rsync-time-backup" target="_blank" rel="noopener"&gt;laurent22/rsync-time-backup&lt;/a&gt;, a popular 600-line Bash script for Time Machine-style backups. While it worked, it had known bugs and was tricky to maintain due to Bash&amp;rsquo;s limitations. It also lacked tests, type hints (bash duh&amp;hellip;), and proper error handling. So, I did what any reasonable developer would do: rewrote it in Python: &lt;a href="https://github.com/basnijholt/rsync-time-machine.py" target="_blank" rel="noopener"&gt;&lt;code&gt;rsync-time-machine.py&lt;/code&gt;&lt;/a&gt;! 🐍&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I needed reliable backups that worked across platforms and filesystems, unlike Apple&amp;rsquo;s Time Machine.&#10;While many excellent backup tools exist, I was drawn to the simplicity of this approach: it just creates folders named &lt;code&gt;YYYY-MM-DD-HH-MM-SS&lt;/code&gt; containing your files.&#10;No special tools needed for restoring or browsing - just your regular file explorer!&#10;I created a fully tested, typed, and documented Python port that maintains compatibility with the original while adding modern development practices.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Creates incremental backups using hard links (saving space while keeping full snapshots)&lt;/li&gt;&#10;&lt;li&gt;Works on Linux, macOS, and Windows (via WSL or Cygwin)&lt;/li&gt;&#10;&lt;li&gt;Supports local and remote (SSH) backups&lt;/li&gt;&#10;&lt;li&gt;Smart backup expiration strategy&lt;/li&gt;&#10;&lt;li&gt;Built-in safety checks and locking mechanism&lt;/li&gt;&#10;&lt;li&gt;Pretty terminal output&lt;/li&gt;&#10;&lt;li&gt;Absolutely zero dependencies beyond Python itself - it just works!&lt;/li&gt;&#10;&lt;li&gt;Usable as a standalone &lt;code&gt;.py&lt;/code&gt; script or pip installable package&lt;/li&gt;&#10;&lt;li&gt;100% test coverage&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The project reached the front page of &lt;a href="https://news.ycombinator.com/item?id=35933238" target="_blank" rel="noopener"&gt;Y Combinator&amp;rsquo;s Hacker News&lt;/a&gt;, causing its popularity to explode:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;381 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;16 forks&lt;/li&gt;&#10;&lt;li&gt;6 contributors&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://realpython.com/podcasts/rpp/158/#t=1588" target="_blank" rel="noopener"&gt;Featured on Real Python podcast&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Active discussion on Reddit&amp;rsquo;s &lt;a href="https://www.reddit.com/r/commandline/comments/13gtnrp/ported_a_popular_untested_600_line_bash_script_to/" target="_blank" rel="noopener"&gt;r/commandline&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Maintaining compatibility with the original Bash script while improving the codebase&lt;/li&gt;&#10;&lt;li&gt;Making it work on Windows through WSL/Cygwin (surprisingly tricky!)&lt;/li&gt;&#10;&lt;li&gt;Handling various edge cases in filesystem operations&lt;/li&gt;&#10;&lt;li&gt;Implementing a robust backup expiration strategy&lt;/li&gt;&#10;&lt;li&gt;Adding comprehensive test coverage for file operations&lt;/li&gt;&#10;&lt;li&gt;Keeping the codebase dependency-free while maintaining functionality&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes a rewrite is worth it, especially when adding modern development practices&lt;/li&gt;&#10;&lt;li&gt;Good documentation and test coverage are crucial for backup tools&lt;/li&gt;&#10;&lt;li&gt;The Python community appreciates well-structured ports of useful tools&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies means zero headaches for users&lt;/li&gt;&#10;&lt;li&gt;Cross-platform compatibility (especially Windows) requires careful consideration&lt;/li&gt;&#10;&lt;li&gt;Simple, transparent solutions (like regular folders) can be better than complex ones&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want reliable, cross-platform Time Machine-style backups? Check out &lt;a href="https://github.com/basnijholt/rsync-time-machine.py" target="_blank" rel="noopener"&gt;the project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Backups #DevTools #Programming&lt;/p&gt;&#10;</description></item><item><title>Adaptive Lighting 💡</title><link>https://www.nijho.lt/post/advent-of-open-source/02-adaptive-lighting/</link><pubDate>Mon, 02 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/02-adaptive-lighting/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Unlike &lt;a href="../01-calendar-of-life"&gt;yesterday&amp;rsquo;s Calendar of Life&lt;/a&gt; project, today we&amp;rsquo;re looking at one of my most popular contributions. It&amp;rsquo;s one of several Home Assistant and home automation related projects (&lt;a href="../08-home-assistant-stream-deck-yaml"&gt;Day 8&lt;/a&gt;, &lt;a href="../10-home-assistant-config"&gt;Day 10&lt;/a&gt;, &lt;a href="../24-home-assistant-ecosystem"&gt;Day 24&lt;/a&gt;) I&amp;rsquo;ll be sharing this month. While my coding journey started in scientific computing with physics simulations, discovering how to control my home environment through code sparked a new obsession. Home Assistant, currently the &lt;strong&gt;#1&lt;/strong&gt; open source project on GitHub, is the brain of my smart home and lets you control and automate everything from lights to thermostats, completely locally and privacy-focused.&lt;/p&gt;&#10;&lt;p&gt;Keep your lighting in sync with the sun, they said. It&amp;rsquo;ll be simple, they said&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2020, inspired by @claytonjn&amp;rsquo;s circadian_lighting Home Assistant component, I wanted to create a more comprehensive solution for automatically adjusting lights based on the time of day. What started as a contribution attempt to Home Assistant Core evolved into its own custom component when the PR wasn&amp;rsquo;t merged - handling all the edge cases made the code too complex for core. The standalone component gave us the freedom to implement features properly, becoming one of Home Assistant&amp;rsquo;s most installed custom integrations.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While the concept is simple, making it work reliably is&amp;hellip; interesting:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Handles unreliable Zigbee networks and flaky WiFi connections&lt;/li&gt;&#10;&lt;li&gt;Manages different light brands&amp;rsquo; quirks (looking at you, IKEA bulbs 👀)&lt;/li&gt;&#10;&lt;li&gt;Provides smooth transitions between color temperatures&lt;/li&gt;&#10;&lt;li&gt;Detects manual overrides without disrupting user control&lt;/li&gt;&#10;&lt;li&gt;Supports sleep modes for bedtime routines&lt;/li&gt;&#10;&lt;li&gt;Configurable via YAML or the UI (only integration to support this!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The integration has 38 parameters to fine-tune your lighting. Try our &lt;a href="https://basnijholt.github.io/adaptive-lighting/" target="_blank" rel="noopener"&gt;WebAssembly simulator&lt;/a&gt; to visualize your settings!&lt;/p&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;2000+ GitHub stars&lt;/li&gt;&#10;&lt;li&gt;103 contributors&lt;/li&gt;&#10;&lt;li&gt;512 issues&lt;/li&gt;&#10;&lt;li&gt;Tens of thousands of active users&lt;/li&gt;&#10;&lt;li&gt;Available in 20+ languages&lt;/li&gt;&#10;&lt;li&gt;One of the most installed custom integrations&lt;/li&gt;&#10;&lt;li&gt;Featured in several YouTube videos and podcasts&lt;/li&gt;&#10;&lt;li&gt;Dedicated parameter visualization web app&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-we-solved"&gt;🎯 Challenges We Solved &lt;a class="anchor" href="#-challenges-we-solved" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Lights randomly turning on (bulbs falsely reporting state)&lt;/li&gt;&#10;&lt;li&gt;Network congestion (solved with Zigbee grouping)&lt;/li&gt;&#10;&lt;li&gt;Different brands interpreting colors differently&lt;/li&gt;&#10;&lt;li&gt;Handling manual overrides gracefully&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&amp;ldquo;It works on my setup&amp;rdquo; is just the beginning&lt;/li&gt;&#10;&lt;li&gt;Users surprise you (like using it for plants, or above the polar circle!)&lt;/li&gt;&#10;&lt;li&gt;Edge cases multiply quickly&lt;/li&gt;&#10;&lt;li&gt;Documentation is as important as code&lt;/li&gt;&#10;&lt;li&gt;Open source thrives on community effort&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want your lights to follow the sun? Check out the &lt;a href="https://github.com/basnijholt/adaptive-lighting" target="_blank" rel="noopener"&gt;project on GitHub&lt;/a&gt; and try the &lt;a href="https://basnijholt.github.io/adaptive-lighting/" target="_blank" rel="noopener"&gt;simulator&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #SmartHome #IoT #Python&lt;/p&gt;&#10;</description></item><item><title>Calendar of Life 📅</title><link>https://www.nijho.lt/post/advent-of-open-source/01-calendar-of-life/</link><pubDate>Sun, 01 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/01-calendar-of-life/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;.)&lt;/p&gt;&#10;&lt;p&gt;Visualizing the finite weeks of our lives, because apparently, I needed another reminder of my mortality!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You know how sometimes you&amp;rsquo;re just casually browsing the internet, not looking to have an existential crisis? Well, that&amp;rsquo;s exactly what happened when I stumbled upon Tim Urban&amp;rsquo;s Wait But Why post &amp;ldquo;Your Life in Weeks&amp;rdquo; and saw Kurzgesagt&amp;rsquo;s &amp;ldquo;Calendar of Your Life&amp;rdquo; poster. Instead of just feeling mildly anxious about the finite nature of existence like a normal person, I decided to cope by writing some Python code. Because that&amp;rsquo;s what we do, right?&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The project uses matplotlib to create a grid where:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Each row represents one year (up to 80 years, optimistically speaking!)&lt;/li&gt;&#10;&lt;li&gt;Each column represents one week (52 weeks per year)&lt;/li&gt;&#10;&lt;li&gt;Different colors show different life stages (early childhood, school, university, etc.)&lt;/li&gt;&#10;&lt;li&gt;The current week blinks in the animated version, just in case you needed a more urgent reminder of time passing&lt;/li&gt;&#10;&lt;li&gt;Supports both dark and light modes, because existential dread comes in all themes&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;One interesting technical detail is handling the fact that 52 weeks isn&amp;rsquo;t exactly one year. The code uses a small correction factor (&lt;code&gt;days_per_week = 365.25 / 52&lt;/code&gt;) to ensure the visualization stays accurate over many years. Yes, I made sure your existential crisis would be mathematically precise.&lt;/p&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With its whopping 5 GitHub stars, this project clearly hasn&amp;rsquo;t taken the world by storm. But hey, sometimes the best tools are the ones we make for ourselves, right?&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The main challenges were (this was actually a very simple one):&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Accurately mapping life events to week numbers (turns out life doesn&amp;rsquo;t fit neatly into boxes)&lt;/li&gt;&#10;&lt;li&gt;Creating smooth animations for the current week indicator (because a blinking dot really drives home the whole &amp;ldquo;time is passing&amp;rdquo; thing)&lt;/li&gt;&#10;&lt;li&gt;Handling the 52 vs 52.18 weeks per year discrepancy (thanks, Earth&amp;rsquo;s orbit)&lt;/li&gt;&#10;&lt;li&gt;Making the visualization work well in both dark and light modes (for viewing your life calendar at any time of day)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This project taught me that sometimes the most impactful tools are the simplest ones. Sure, it&amp;rsquo;s just a bunch of dots on a grid, but each dot represents a week of life - past, present, or future. It&amp;rsquo;s a humbling reminder that while we can&amp;rsquo;t add more dots to our grid, we can try to make each one count.&lt;/p&gt;&#10;&lt;p&gt;Want to contemplate your own mortality in a more organized way? Check out the &lt;a href="https://github.com/basnijholt/calendar-of-life" target="_blank" rel="noopener"&gt;project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DataVisualization #Programming&lt;/p&gt;&#10;</description></item></channel></rss>