<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Software Development | Bas Nijholt</title><link>https://www.nijho.lt/category/software-development/</link><atom:link href="https://www.nijho.lt/category/software-development/index.xml" rel="self" type="application/rss+xml"/><description>Software Development</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>Software Development</title><link>https://www.nijho.lt/category/software-development/</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>Why Docker isn't an AI-safe sandbox anymore, and how I fixed it in MindRoom 🔒</title><link>https://www.nijho.lt/post/kernels-containers-vms/</link><pubDate>Sun, 18 Oct 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/kernels-containers-vms/</guid><description>&lt;p&gt;For years, a Docker container was my default answer to &amp;ldquo;where do I run this?&amp;rdquo;&#10;I never thought it was a perfect sandbox.&#10;It was good enough, because getting out of one usually meant finding a specific kind of bug in the Linux kernel itself, and those were hard to find.&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;AI models have become very good at finding security bugs, including in Linux.&#10;Anthropic and OpenAI now keep their strongest security models, &lt;a href="https://www.anthropic.com/claude/mythos" target="_blank" rel="noopener"&gt;Claude Mythos&lt;/a&gt; and &lt;a href="https://openai.com/the-defense-factory/" target="_blank" rel="noopener"&gt;Daybreak Blue&lt;/a&gt;, behind identity checks and vetting.&#10;Open-weight models, which anyone can download and run, come with no such rules, and they trail the closed ones by only &lt;a href="https://epoch.ai/data-insights/open-closed-cyber-eci-gap" target="_blank" rel="noopener"&gt;six to seven months&lt;/a&gt;.&#10;Once there is an open model as good as Mythos, anyone can strip out its refusals with a tool like &lt;a href="https://github.com/p-e-w/heretic" target="_blank" rel="noopener"&gt;Heretic&lt;/a&gt; (people call this abliteration) and point it at the Linux kernel.&#10;The security company DepthFirst used its own model to find a way out of a container and titled the write-up &lt;a href="https://depthfirst.com/research/containers-are-no-longer-safe" target="_blank" rel="noopener"&gt;Containers Are No Longer a Security Boundary&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I care about this because running untrusted code in containers is exactly what &lt;a href="https://www.nijho.lt/post/mindroom/"&gt;MindRoom&lt;/a&gt;, my open-source system for AI agents that live in Matrix chat rooms, does.&#10;Every agent can get its own Docker worker, and its shell, file, Python, and browser tools run there.&#10;It can install anything, start background processes, and drive a real browser.&#10;And as I wrote in &lt;a href="https://www.nijho.lt/post/btrfs-to-zfs/"&gt;my ZFS post&lt;/a&gt;, I run coding agents in &lt;span class="hover-tip" data-tooltip="The agent runs every command without asking me for approval first." tabindex="0"&gt;YOLO mode&lt;/span&gt;, exclusively.&#10;I do not review the commands they run, I look at the results.&lt;/p&gt;&#10;&lt;p&gt;So both sides changed at once.&#10;The code inside the container is now written by an AI, and the bugs that let it out are now found by an AI.&#10;A &lt;a href="https://youtu.be/9-2CSJC114k" target="_blank" rel="noopener"&gt;video from the Low Level channel&lt;/a&gt; asking whether containers are still safe made me sit down and rethink my setup.&lt;/p&gt;&#10;&lt;p&gt;I am not an expert in any of this.&#10;I have used containers, KVM, and hypervisors for years, including Proxmox before I &lt;a href="https://www.nijho.lt/post/proxmox-to-nixos/"&gt;moved to NixOS&lt;/a&gt;, but I never deeply understood what happens underneath.&#10;Building this forced me to learn it, and writing this post is my exercise in understanding it all.&#10;All of this is very complicated (as is everything new), and I am still learning.&#10;I am sure I skip over a lot and simplify the rest; what is here are the parts that helped me understand it.&#10;I thought it would be nice to share.&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; A container is a process on your machine that the kernel dresses up as a separate computer.&#10;Every container shares that one kernel with the host, so a single bug in it can be a way out.&#10;MindRoom can now give each agent&amp;rsquo;s worker its own kernel, with one setting and six lines of code.&lt;/div&gt;&lt;/aside&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="#what-the-kernel-actually-does"&gt;What the kernel actually does&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#a-container-is-a-process-in-a-costume"&gt;A container is a process in a costume&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-kernel-bugs-are-a-bigger-problem-now"&gt;Why kernel bugs are a bigger problem now&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#so-i-asked-a-model-to-break-out"&gt;So I asked a model to break out&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#stronger-walls-virtual-machines"&gt;Stronger walls: virtual machines&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#kata-containers-and-gvisor"&gt;Kata Containers and gVisor&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-pr-six-lines"&gt;The PR: six lines&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-it-costs"&gt;What it costs&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-this-does-not-fix"&gt;What this does not fix&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="what-the-kernel-actually-does"&gt;What the kernel actually does &lt;a class="anchor" href="#what-the-kernel-actually-does" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When people talk about Linux, they usually mean an operating system like Ubuntu, Fedora, or NixOS.&#10;Those differ in almost everything you see: package managers, desktops, config files.&#10;What they all have in common is the kernel, the part Linus Torvalds started in 1991 and still leads today.&#10;Strictly speaking, that kernel is the only part called Linux.&lt;/p&gt;&#10;&lt;p&gt;The kernel is the one program on a Linux machine that talks to the hardware directly.&#10;Everything else, your shell, Python, Chrome, Docker, runs on top of it and has to ask it for things.&#10;Open this file.&#10;Give me some memory.&#10;Send this packet.&#10;Start a new process.&#10;Those requests are called system calls, or syscalls for short.&lt;/p&gt;&#10;&lt;p&gt;There is one kernel per machine, and it decides who is allowed to do what.&#10;That makes it the most trusted code on the system.&#10;Most kernel bugs only crash the machine or cause glitches.&#10;But some let a normal program become root, the admin account, or even run its own code inside the kernel.&#10;At that point it can do anything on the machine, including leaving any container it was in.&lt;/p&gt;&#10;&lt;h2 id="a-container-is-a-process-in-a-costume"&gt;A container is a process in a costume &lt;a class="anchor" href="#a-container-is-a-process-in-a-costume" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When you &lt;code&gt;docker run -it ubuntu&lt;/code&gt;, you get a root shell in what feels like a fresh computer.&#10;Under the hood it is one ordinary process.&#10;The kernel dresses it up using three features:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Namespaces&lt;/strong&gt; decide what the process can see. It gets its own list of processes (where it is process number 1), its own filesystem, and its own network. Root inside is still root on the host, with &lt;span class="hover-tip" data-tooltip="Docker drops most of root&amp;#39;s special powers, which Linux calls capabilities, and seccomp and AppArmor block more. Namespaces only change what root can see." tabindex="0"&gt;most of its powers taken away&lt;/span&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;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;cgroups&lt;/strong&gt; decide how much it can use: CPU, memory, number of processes.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;seccomp&lt;/strong&gt; decides which syscalls it may make at all. It is a filter that blocks the ones a normal program never needs.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;figure&gt;&lt;img src="https://www.nijho.lt/post/kernels-containers-vms/container-kernel.svg" alt="Two containers and the host&amp;#39;s own processes all make syscalls into one shared Linux kernel, which keeps the containers apart with namespaces, cgroups and seccomp. One bug in that kernel reaches every container and the host." loading="lazy" data-zoomable&gt;&lt;/figure&gt;&#10;&lt;p&gt;There is no second kernel.&#10;Every syscall from inside a container lands in the same kernel as every other container and the host itself.&#10;The kernel enforces the isolation, and it is also what gets attacked.&lt;/p&gt;&#10;&lt;p&gt;For most uses that is fine.&#10;Usually you put fixed software in a container: a database, a web app, some self-hosted service.&#10;You know where that code comes from, you keep your kernel patched, and that is good enough.&lt;/p&gt;&#10;&lt;p&gt;An AI agent is different.&#10;It writes new code on the spot and runs it, and nobody knows in advance what that code will do.&lt;/p&gt;&#10;&lt;p&gt;Since root in a container is root on the host, anyone who can use Docker can start a container as root and mount any folder from the host into it.&#10;They can then read and change every file in that folder, including files their own account may not open.&#10;Docker&amp;rsquo;s own documentation says the &lt;code&gt;docker&lt;/code&gt; group &lt;a href="https://docs.docker.com/engine/install/linux-postinstall/" target="_blank" rel="noopener"&gt;grants root-level privileges&lt;/a&gt;.&#10;I knew that in theory, but it only clicked when I watched an AI agent do it on a machine where I had no root, only Docker access.&#10;It needed to change files it was not allowed to touch, so it started a small container, mounted their folder, and edited them from inside.&#10;No kernel bug needed.&lt;/p&gt;&#10;&lt;h2 id="why-kernel-bugs-are-a-bigger-problem-now"&gt;Why kernel bugs are a bigger problem now &lt;a class="anchor" href="#why-kernel-bugs-are-a-bigger-problem-now" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Kernel bugs that let a normal program take over the whole machine used to take skilled people weeks or months to find and exploit.&#10;The famous ones, like &lt;a href="https://dirtycow.ninja" target="_blank" rel="noopener"&gt;Dirty COW&lt;/a&gt; in 2016, made the news every year or two, you patched, and that was it.&lt;/p&gt;&#10;&lt;p&gt;AI is speeding this up, and the DepthFirst write-up I mentioned is a good example.&#10;Their model found a bug in the kernel code for Unix sockets, which programs on the same machine use to talk to each other.&#10;The kernel freed a piece of memory and then kept using it, a so-called use-after-free, and from inside a container that was enough to get out.&#10;Their conclusion is that you should assume attackers can escape containers at will.&lt;/p&gt;&#10;&lt;p&gt;Blocking more syscalls, or taking more powers away from root, helps a little.&#10;But it only narrows which parts of the shared kernel a container can reach.&#10;This bug sat in Unix sockets, which almost every program uses, including the browser the agent drives, so blocking them would break the agent.&lt;/p&gt;&#10;&lt;p&gt;Now put an AI agent in that container.&#10;It reads web pages and repositories written by strangers, and one of those could talk it into running an exploit.&#10;On a MindRoom server, getting out of a worker means getting onto the machine that runs MindRoom itself.&#10;That is where the API keys, Matrix logins, and stored credentials live, which the workers are built to never see.&lt;/p&gt;&#10;&lt;h2 id="so-i-asked-a-model-to-break-out"&gt;So I asked a model to break out &lt;a class="anchor" href="#so-i-asked-a-model-to-break-out" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before changing anything, I wanted to know how real this was for my own setup.&#10;I gave a MindRoom agent &lt;a href="https://openai.com/the-defense-factory/" target="_blank" rel="noopener"&gt;Daybreak Blue&lt;/a&gt; and asked it to escape its worker container using known kernel vulnerabilities.&#10;Daybreak Blue is OpenAI&amp;rsquo;s security model for verified users, so it does the offensive security work that other models refuse.&lt;/p&gt;&#10;&lt;p&gt;It did not get out, which was reassuring.&#10;But it only tried known bugs.&#10;A kernel a few weeks behind on patches, or a bug nobody has published yet, would be a different story.&#10;So the attempt tells me the easy path was closed that day, and not much more.&lt;/p&gt;&#10;&lt;h2 id="stronger-walls-virtual-machines"&gt;Stronger walls: virtual machines &lt;a class="anchor" href="#stronger-walls-virtual-machines" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The classic answer is a virtual machine.&#10;A container shares the host&amp;rsquo;s kernel, but a VM brings its own, and this is where the hardware comes in.&#10;Modern CPUs have a virtualization mode built in, called VT-x on Intel and AMD-V on AMD.&#10;In that mode, a second kernel, the guest kernel, runs directly on the real CPU and believes it is in charge, while the CPU quietly limits it.&#10;When the guest tries something it may not do on its own, like talking to a disk or a network card, the CPU pauses the guest and hands control to the host.&#10;This is called a VM exit.&#10;Despite the name, nothing escapes and nothing crashes.&#10;The host handles the request, for example by pretending to be that disk, and then resumes the guest where it stopped.&#10;From the guest&amp;rsquo;s point of view, the instruction simply finished.&lt;/p&gt;&#10;&lt;p&gt;Memory is fenced off too.&#10;The host gives the CPU a separate map of which real memory each guest may use, and the CPU checks every memory access against it.&#10;So even a guest kernel with full privileges cannot touch the host&amp;rsquo;s memory.&lt;/p&gt;&#10;&lt;p&gt;The CPU only provides the mechanism.&#10;The software that sets the rules and handles the VM exits is called the hypervisor.&#10;&amp;ldquo;Hypervisor&amp;rdquo; is a role, not one program, and on Linux it is two pieces:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;KVM&lt;/strong&gt;, a module in the Linux kernel, uses the CPU&amp;rsquo;s virtualization mode to run the guest, keeps that memory map, and catches its VM exits. It shows up as the device &lt;code&gt;/dev/kvm&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;A VMM&lt;/strong&gt; (virtual machine monitor), a normal program on top, sets up the VM and handles the exits that KVM passes up, for example by pretending to be the guest&amp;rsquo;s disk and network card. &lt;a href="https://www.qemu.org" target="_blank" rel="noopener"&gt;QEMU&lt;/a&gt;, &lt;a href="https://firecracker-microvm.github.io" target="_blank" rel="noopener"&gt;Firecracker&lt;/a&gt;, and &lt;a href="https://www.cloudhypervisor.org" target="_blank" rel="noopener"&gt;Cloud Hypervisor&lt;/a&gt; are three VMMs you can pick from.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Proxmox, which I mentioned above, is KVM plus QEMU.&#10;AWS Lambda is KVM plus Firecracker.&lt;/p&gt;&#10;&lt;p&gt;Code in a VM talks to its own guest kernel.&#10;To reach the host, it usually has to break that kernel first and then the hypervisor.&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;&#10;That is still possible.&#10;Google Project Zero turned &lt;a href="https://googleprojectzero.blogspot.com/2021/06/an-epyc-escape-case-study-of-kvm.html" target="_blank" rel="noopener"&gt;a bug in how KVM runs VMs inside VMs on AMD&lt;/a&gt; into a full escape in 2021.&#10;Google&amp;rsquo;s &lt;a href="https://security.googleblog.com/2024/06/virtual-escape-real-reward-introducing.html" target="_blank" rel="noopener"&gt;kvmCTF&lt;/a&gt; pays up to &lt;span class="hover-tip" data-tooltip="The top reward in the current kvmCTF rules, for a full escape from the guest VM to the host." tabindex="0"&gt;$250,000&lt;/span&gt; for a new one.&#10;And this year, one researcher, Hyunwoo Kim, published &lt;a href="https://github.com/V4bel/Zapscape" target="_blank" rel="noopener"&gt;three KVM escapes&lt;/a&gt;, one of them hidden in KVM &lt;span class="hover-tip" data-tooltip="Januscape, CVE-2026-53359: a use-after-free in the shadow page tables of x86 KVM, added in August 2010 and fixed in June 2026." tabindex="0"&gt;since 2010&lt;/span&gt;.&lt;/p&gt;&#10;&lt;p&gt;The difference between a container and a VM is how much code an attacker can reach, and that gap is large.&#10;On x86 (the Intel and AMD chips in most PCs and servers), Linux has &lt;span class="hover-tip" data-tooltip="The x86_64 syscall table of Linux 7.3-rc7 has 386 entries for 64-bit programs, and 16 of them are empty slots that only return an error." tabindex="0"&gt;about 370 syscalls&lt;/span&gt;, and Docker&amp;rsquo;s &lt;a href="https://docs.docker.com/engine/security/seccomp/" target="_blank" rel="noopener"&gt;default seccomp filter&lt;/a&gt; blocks &lt;span class="hover-tip" data-tooltip="Docker&amp;#39;s documentation says its default profile disables around 44 system calls out of 300&amp;#43;. The current profile blocks a few more, like the io_uring calls." tabindex="0"&gt;a few dozen&lt;/span&gt; of them.&#10;Behind the rest sit the kernel&amp;rsquo;s filesystems, networking, memory management, and core code: &lt;span class="hover-tip" data-tooltip="The fs, net, mm and kernel folders of Linux 7.3-rc7, counted with tokei, comments and blank lines included. The whole tree is 43 million lines, 26.5 million of them drivers." tabindex="0"&gt;about 3.8 million lines&lt;/span&gt;, even without counting drivers.&#10;Not all of that is reachable from a container, but a lot of it is.&#10;KVM on x86 is &lt;span class="hover-tip" data-tooltip="arch/x86/kvm plus virt/kvm in the same Linux 7.3-rc7 tree, counted the same way: 113,720 lines." tabindex="0"&gt;about 114,000 lines&lt;/span&gt;.&#10;On top of KVM comes the VMM, but only the few devices it fakes for the guest.&#10;Most QEMU escapes came from its many emulated devices, which is why Firecracker and Cloud Hypervisor keep that list short.&lt;/p&gt;&#10;&lt;p&gt;Google&amp;rsquo;s bug bounties point the same way.&#10;&lt;a href="https://google.github.io/security-research/kernelctf/rules.html" target="_blank" rel="noopener"&gt;kernelCTF&lt;/a&gt; pays &lt;span class="hover-tip" data-tooltip="One fixed amount per accepted exploit, according to the current kernelCTF rules." tabindex="0"&gt;$71,337&lt;/span&gt; for each new kernel exploit that turns a normal user into root, and Google&amp;rsquo;s &lt;a href="https://github.com/google/security-research/tree/master/pocs/linux/kernelctf" target="_blank" rel="noopener"&gt;public repository&lt;/a&gt; holds exploits for &lt;span class="hover-tip" data-tooltip="119 exploit folders in pocs/linux/kernelctf, covering 93 unique CVEs, at commit 79f049dc from October 16, 2026." tabindex="0"&gt;93 different kernel bugs&lt;/span&gt;.&#10;kvmCTF pays more than three times as much, and the same repository holds &lt;span class="hover-tip" data-tooltip="The only folder in pocs/linux/kvmctf at the same commit: a NULL pointer dereference reached through nested virtualization. Januscape was also used in kvmCTF but is not in the repository yet." tabindex="0"&gt;one KVM exploit&lt;/span&gt;, which crashes the host instead of escaping it.&#10;kvmCTF started a year later, but the gap is still big.&#10;Most of those kernel exploits would not work as written from a default container, because they &lt;span class="hover-tip" data-tooltip="84 of the 119 exploit folders create a user namespace, which gives a normal process root&amp;#39;s powers over its own set of namespaces and opens up kernel code it could not reach otherwise. Docker&amp;#39;s default seccomp profile blocks that unless the container has the CAP_SYS_ADMIN capability." tabindex="0"&gt;first use a feature Docker blocks&lt;/span&gt;.&#10;Still, they show how many exploitable bugs sit behind the syscalls.&lt;/p&gt;&#10;&lt;h2 id="kata-containers-and-gvisor"&gt;Kata Containers and gVisor &lt;a class="anchor" href="#kata-containers-and-gvisor" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Regular VMs are heavy.&#10;They boot a full operating system and tend to use a lot of memory.&#10;A microVM is a stripped-down VM meant to run one workload and start quickly.&#10;That still leaves a practical problem.&#10;MindRoom, like most tools, speaks Docker: it hands Docker an image and some folders to mount, and gets a running container back.&#10;A VM does not take a Docker image.&#10;Switching to VMs would normally mean building disk images, booting them, and wiring up files and networking myself.&lt;/p&gt;&#10;&lt;p&gt;Docker does not start containers by itself, though.&#10;It takes care of images, networks, and volumes, and then hands the actual start to a small program called a runtime.&#10;The default runtime, &lt;code&gt;runc&lt;/code&gt;, does what I described earlier: it starts a process on the host kernel with namespaces, cgroups, and seccomp.&#10;But you can tell Docker to use a different runtime.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://katacontainers.io" target="_blank" rel="noopener"&gt;Kata Containers&lt;/a&gt; is such a runtime, and it starts a microVM instead.&#10;It picks one of the VMMs above (my test used QEMU), boots a small guest kernel, and starts the container inside it.&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;Docker, and MindRoom, still see an ordinary container.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://gvisor.dev" target="_blank" rel="noopener"&gt;gVisor&lt;/a&gt;, from Google, takes a different route, and it is not a VM.&#10;It is a reimplementation of the Linux kernel, written in Go, that runs as a normal program on the host.&#10;Go manages memory for the programmer, so the use-after-free mistake behind the DepthFirst escape is much harder to make in gVisor.&#10;The worker&amp;rsquo;s syscalls go to gVisor instead of the host kernel, and gVisor itself only makes a small, filtered set of host syscalls.&#10;That is the hardening idea taken all the way: gVisor answers the agent&amp;rsquo;s syscalls itself, so the filter on the host can be much stricter than Docker&amp;rsquo;s.&#10;It needs no &lt;code&gt;/dev/kvm&lt;/code&gt;, so it also runs inside cloud VMs that cannot start VMs of their own.&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;&#10;My test machine, an ARM server (with the kind of chip phones use) running NixOS, had no &lt;code&gt;/dev/kvm&lt;/code&gt;, which is why I tried gVisor first.&#10;Its runtime is called &lt;code&gt;runsc&lt;/code&gt;, and it plugs into Docker the same way.&lt;/p&gt;&#10;&lt;figure id="figure-the-three-options-side-by-side"&gt;&lt;img src="https://www.nijho.lt/post/kernels-containers-vms/isolation-stacks.svg" alt="Three stacks side by side. Plain Docker: the agent&amp;#39;s code makes syscalls straight into the host kernel, so the usual way out is a host kernel bug, reached through any syscall seccomp allows. gVisor: the agent&amp;#39;s code makes syscalls into gVisor&amp;#39;s own kernel, written in Go, which makes a small set of syscalls to the host kernel, so the usual way out is a gVisor bug and then a host kernel bug. Kata Containers: the agent&amp;#39;s code runs on a guest kernel inside a microVM, and the CPU runs that guest kernel directly, next to the host kernel instead of on top of it. When the guest needs a device, a VM exit hands control to the KVM module in the host kernel, which passes the request up to the VMM, a normal program on the host that fakes the device. The VMM and KVM together are the hypervisor, so the usual way out is a guest kernel bug and then a hypervisor bug." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The three options side by side.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;Or as a table:&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;Isolation&lt;/th&gt;&#10;&lt;th&gt;What worker code talks to&lt;/th&gt;&#10;&lt;th&gt;Needs KVM&lt;/th&gt;&#10;&lt;th&gt;Usual escape route&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Plain Docker (&lt;code&gt;runc&lt;/code&gt;)&lt;/td&gt;&#10;&lt;td&gt;The host kernel, filtered by seccomp&lt;/td&gt;&#10;&lt;td&gt;No&lt;/td&gt;&#10;&lt;td&gt;A host kernel bug&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;gVisor (&lt;code&gt;runsc&lt;/code&gt;)&lt;/td&gt;&#10;&lt;td&gt;gVisor&amp;rsquo;s own kernel, written in Go&lt;/td&gt;&#10;&lt;td&gt;No&lt;/td&gt;&#10;&lt;td&gt;A gVisor bug, then a host kernel bug through its small filtered surface&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Kata Containers&lt;/td&gt;&#10;&lt;td&gt;Its own guest kernel in a microVM&lt;/td&gt;&#10;&lt;td&gt;Yes&lt;/td&gt;&#10;&lt;td&gt;A guest kernel bug, then a hypervisor bug&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Each option also changes how much code on the host an escape can pick its bug from.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Lines of code on the host that deal with what the agent&amp;rsquo;s code asks for, counted with tokei, comments and blank lines included. For Docker, the core of Linux without drivers; for gVisor, its kernel and network stack without tests; for Kata, KVM alone, without the VMM and Kata&amp;rsquo;s other helpers. Not all of it is reachable. Hover for exact numbers." data-plot-module="/post/kernels-containers-vms/plots.js" data-plot-name="attackSurface"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Lines of code on the host that deal with what the agent&amp;rsquo;s code asks for, counted with tokei, comments and blank lines included. For Docker, the core of Linux without drivers; for gVisor, its kernel and network stack without tests; for Kata, KVM alone, without the VMM and Kata&amp;rsquo;s other helpers. Not all of it is reachable. Hover for exact numbers.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;h2 id="the-pr-six-lines"&gt;The PR: six lines &lt;a class="anchor" href="#the-pr-six-lines" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The change is &lt;a href="https://github.com/mindroom-ai/mindroom/pull/2886" target="_blank" rel="noopener"&gt;PR #2886&lt;/a&gt;, which is now merged.&#10;It adds one setting, &lt;code&gt;MINDROOM_DOCKER_WORKER_RUNTIME&lt;/code&gt;, and MindRoom passes its value to Docker as the runtime whenever it starts a worker.&#10;That is &lt;span class="hover-tip" data-tooltip="Three lines in docker.py and three in docker_config.py. The merged PR adds 98 lines: these 6, 62 lines of tests, and 30 lines of docs." tabindex="0"&gt;six lines of source code&lt;/span&gt;, plus 62 lines of tests and some docs.&#10;Changing the setting recreates each worker the next time it is used, and leaving it unset keeps everything as it was.&lt;/p&gt;&#10;&lt;p&gt;For gVisor, setting it up is two steps: register the runtime in Docker&amp;rsquo;s &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt; and restart Docker, then point MindRoom at it.&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;runtimes&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="nt"&gt;&amp;#34;runsc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;path&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/usr/local/bin/runsc&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;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="nv"&gt;MINDROOM_DOCKER_WORKER_RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;runsc&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Kata needs no entry in that file: once it is installed, &lt;code&gt;MINDROOM_DOCKER_WORKER_RUNTIME=io.containerd.kata.v2&lt;/code&gt; is enough.&#10;The full setup, and a few catches for each runtime, are in the &lt;a href="https://docs.mindroom.chat/deployment/sandbox-proxy/#security-considerations" target="_blank" rel="noopener"&gt;MindRoom docs&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The six lines are short because everything else already existed.&#10;gVisor and Kata do the isolating.&#10;Docker already gives each worker its folders, its credentials, and its network, and it already knows how to hand a container to a different runtime.&#10;On the MindRoom side, the hard part happened months earlier, when agent tools moved out of the main process into dedicated workers with explicit mounts and credentials.&#10;Once every worker is a single call to Docker&amp;rsquo;s &lt;code&gt;containers.run&lt;/code&gt;, swapping what runs underneath it is one argument.&#10;For comparison, Claude Opus estimated that building a microVM backend into MindRoom itself would take &lt;span class="hover-tip" data-tooltip="For a native Cloud Hypervisor backend that rebuilds image conversion, guest init, folder sharing, networking, and VM supervision. The estimate is in the PR description." tabindex="0"&gt;2,500 to 4,000 lines&lt;/span&gt;.&lt;/p&gt;&#10;&lt;h2 id="what-it-costs"&gt;What it costs &lt;a class="anchor" href="#what-it-costs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I tested both runtimes with a real agent in a Matrix room: gVisor on that ARM server, and Kata &lt;span class="hover-tip" data-tooltip="Kata 4.2 with QEMU and Docker 29.9, inside an Incus VM with nested KVM on my desktop&amp;#39;s Ryzen 9 3900X." tabindex="0"&gt;in a throwaway VM on my desktop&lt;/span&gt;.&#10;Shell, Python, git, background processes, and the browser all worked, without any MindRoom code change.&#10;My Kata numbers below come from a VM inside a VM, so bare-metal numbers will differ.&lt;/p&gt;&#10;&lt;p&gt;Configuration is the cheap part: one environment variable, plus one entry in Docker&amp;rsquo;s config for gVisor.&#10;The other costs are the usual trade-offs between containers and VMs:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Computation.&lt;/strong&gt; Code that only calculates, without asking the kernel for anything, runs at full speed with both. gVisor lets those instructions run directly on the CPU, according to its &lt;a href="https://gvisor.dev/docs/architecture_guide/performance/" target="_blank" rel="noopener"&gt;performance guide&lt;/a&gt;. A Kata guest runs on the CPU in its virtualization mode, and in &lt;a href="https://www.usenix.org/conference/nsdi20/presentation/agache" target="_blank" rel="noopener"&gt;AWS&amp;rsquo;s paper on Firecracker&lt;/a&gt;, the CPU overhead was &lt;span class="hover-tip" data-tooltip="The paper reports memory overhead as low as 3% and negligible CPU overhead." tabindex="0"&gt;negligible&lt;/span&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Syscalls, disk, and network.&lt;/strong&gt; The cost starts when a program does ask the kernel for something. Under gVisor, every syscall takes a detour through gVisor&amp;rsquo;s kernel, so programs that make many small syscalls, like ones that open thousands of small files, slow down the most. Under Kata, disk and network go through the VMM&amp;rsquo;s fake devices. In that same paper from 2020, a Firecracker guest reached &lt;span class="hover-tip" data-tooltip="iperf3 over a tap device on an EC2 m5d.metal host, one stream: 44.1 Gbit/s for the host, 15.6 for Firecracker, and 23.8 for QEMU." tabindex="0"&gt;15 Gbit/s of network traffic where the host reached 44&lt;/span&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Startup.&lt;/strong&gt; gVisor only has to start a program, so it starts quickly. Kata has to boot a kernel first, and my worker took &lt;span class="hover-tip" data-tooltip="From creating the container until the worker answered its health check, three runs each: 10.1 to 10.9 seconds with Kata, 2.4 to 3.3 with runc." tabindex="0"&gt;about 10 seconds to start instead of about 3&lt;/span&gt;. Booting a VM can be much faster than that: &lt;a href="https://firecracker-microvm.github.io" target="_blank" rel="noopener"&gt;Firecracker&lt;/a&gt; boots a minimal one in about 125 ms.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Memory.&lt;/strong&gt; gVisor adds a &lt;span class="hover-tip" data-tooltip="gVisor&amp;#39;s performance guide: the sandbox itself is responsible for a small, mostly fixed amount of memory overhead." tabindex="0"&gt;small, mostly fixed amount&lt;/span&gt; per sandbox. Each Kata VM runs a second kernel plus the VMM, which alone takes &lt;span class="hover-tip" data-tooltip="Figure 7 of the Firecracker paper: memory used by the VMM process on top of the guest&amp;#39;s own memory, about 3 MB for Firecracker, 13 MB for Cloud Hypervisor, and 131 MB for QEMU." tabindex="0"&gt;about 3 MB with Firecracker and 131 MB with QEMU&lt;/span&gt;. Kata gives each worker VM &lt;span class="hover-tip" data-tooltip="Kata&amp;#39;s default VM has 1 vCPU and 2 GiB, set by default_vcpus and default_memory in its configuration file. Kata adds a container&amp;#39;s memory limit on top, but MindRoom sets none for its workers. The worker&amp;#39;s 1 GiB /tmp counts against it." tabindex="0"&gt;2 GiB by default&lt;/span&gt;, and &lt;code&gt;/tmp&lt;/code&gt; lives in that RAM. The host only hands out memory the guest actually touches, so my idle worker used &lt;span class="hover-tip" data-tooltip="QEMU used 530 to 616 MiB and virtiofsd, which shares the folders, about 160 MiB for one idle worker VM. Both map the guest&amp;#39;s memory, so some pages count twice and the real total is lower." tabindex="0"&gt;under 0.8 GiB&lt;/span&gt; of host memory. Raising the VM size &lt;span class="hover-tip" data-tooltip="The guest kernel keeps 64 bytes of bookkeeping for every 4 KiB page, about 1.6% of the VM&amp;#39;s size, so an 8 GiB VM costs about 128 MiB up front." tabindex="0"&gt;costs little&lt;/span&gt; until a worker needs it. But a VM cannot grow past its size the way a container can, and memory the guest has used, for example to keep recently read files in its cache, &lt;span class="hover-tip" data-tooltip="Kata&amp;#39;s reclaim_guest_freed_memory option, off by default, returns pages the guest frees. File cache does not count as free, so it stays." tabindex="0"&gt;mostly stays with the VM&lt;/span&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Compatibility.&lt;/strong&gt; gVisor reimplements Linux, and its &lt;a href="https://gvisor.dev/docs/user_guide/compatibility/" target="_blank" rel="noopener"&gt;docs&lt;/a&gt; say there will always be unimplemented features and bugs, so now and then a program will not work. Kata runs a real Linux kernel, so programs behave as usual, with a few catches. For example, a file lock held on the host does not block the worker, because the lock lives in the guest kernel.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="what-this-does-not-fix"&gt;What this does not fix &lt;a class="anchor" href="#what-this-does-not-fix" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;A stronger sandbox protects the host.&#10;It does nothing about the tokens and network access you give the worker.&#10;An agent needs no kernel exploit to misuse a token that can push to your repositories, or to poke at every device on your home network.&#10;Scoped credentials and a firewall allowlist protect you at least as much, and MindRoom handles both separately: &lt;a href="https://docs.mindroom.chat/deployment/egress-broker/" target="_blank" rel="noopener"&gt;Agent Keys&lt;/a&gt; let agents use API keys without ever seeing them, and &lt;a href="https://docs.mindroom.chat/deployment/approved-egress/" target="_blank" rel="noopener"&gt;Approved Egress&lt;/a&gt; limits which servers a worker on Kubernetes can reach.&lt;/p&gt;&#10;&lt;p&gt;gVisor has had its own bugs, and it still talks to the host kernel, just through a much smaller door.&#10;KVM has bugs too, as this year&amp;rsquo;s escapes show.&#10;Both walls make an escape far harder, and neither makes it impossible.&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;Once I started reading about this, I kept running into people saying containers were never a security boundary.&#10;That is true, and for years I did not worry about it.&#10;With AI finding kernel bugs faster and agents running code nobody reads, I do now.&lt;/p&gt;&#10;&lt;p&gt;No sandbox is perfectly safe, though.&#10;In &lt;a href="https://www.dwarkesh.com/p/noam-brown" target="_blank" rel="noopener"&gt;a conversation with Dwarkesh Patel&lt;/a&gt;, Noam Brown doubted that even computers with no network connection at all would be enough to contain an AI.&#10;He pointed to &lt;a href="https://arxiv.org/abs/1503.07919" target="_blank" rel="noopener"&gt;studies&lt;/a&gt; where two such computers next to each other &lt;span class="hover-tip" data-tooltip="BitWhisper, from Ben-Gurion University in 2015: 1 to 8 bits per hour between computers up to 40 cm apart, enough for a short command or a password." tabindex="0"&gt;talk through heat&lt;/span&gt;: one runs its CPU hot, and the other picks up the change with its temperature sensor.&#10;Every option in this post trades some speed or memory for safety.&#10;How far to go depends on your own risk tolerance and how paranoid you are.&#10;MindRoom can now move its agents off the shared kernel with one environment variable.&#10;I do not use it myself yet, but if you are more paranoid than I am, it is there.&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;The other way out is a bug in the program that starts the container. In 2024, &lt;a href="https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv" target="_blank" rel="noopener"&gt;CVE-2024-21626&lt;/a&gt; in runc, the program Docker uses for that, leaked a handle to a host folder into new containers, so a malicious image could reach the host&amp;rsquo;s files without any kernel bug.&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;Docker can map the container&amp;rsquo;s root to an ordinary user on the host with &lt;a href="https://docs.docker.com/engine/security/userns-remap/" target="_blank" rel="noopener"&gt;user namespace remapping&lt;/a&gt;, or run without root altogether in &lt;a href="https://docs.docker.com/engine/security/rootless/" target="_blank" rel="noopener"&gt;rootless mode&lt;/a&gt;, but neither is the default. Rootless mode would have stopped the agent in my Docker story, because Docker then has only my own rights. Remapping would not, because anyone who can use Docker can turn it off for a single container. Neither helps against a kernel bug, because code running inside the kernel is above every user.&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;A bug in the VMM&amp;rsquo;s fake devices, or in other VMM code that handles the guest&amp;rsquo;s traffic, can skip the first step, because a normal program in the guest can feed that code data through the network or a shared folder. &lt;a href="https://access.redhat.com/security/cve/cve-2020-8608" target="_blank" rel="noopener"&gt;CVE-2020-8608&lt;/a&gt;, a buffer overflow in QEMU&amp;rsquo;s user-mode networking, could be triggered that way with crafted network traffic.&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;Like runc for Docker, Kata&amp;rsquo;s own programs on the host are a way out too. In 2020, one bug (&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2020-2023" target="_blank" rel="noopener"&gt;CVE-2020-2023&lt;/a&gt;) let a container take over the guest without a kernel bug, and a second (&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2020-2026" target="_blank" rel="noopener"&gt;CVE-2020-2026&lt;/a&gt;) then let the guest trick the Kata runtime on the host into mounting files over any host path. Neither needed a KVM bug. virtiofsd, the program that shares folders with the guest, also runs on the host and handles the guest&amp;rsquo;s file requests.&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;gVisor can &lt;a href="https://gvisor.dev/docs/architecture_guide/platforms/" target="_blank" rel="noopener"&gt;use KVM too&lt;/a&gt;, as an option. By default it catches the agent&amp;rsquo;s syscalls without it, which is the setup this post describes. With KVM switched on, gVisor runs the agent&amp;rsquo;s code in the CPU&amp;rsquo;s virtualization mode so catching syscalls is faster, but gVisor&amp;rsquo;s own kernel still answers them, and there is no guest Linux and no fake devices. gVisor recommends that mode only on bare metal; inside a VM the default is usually faster.&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;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Giving models the tools they were trained with can make them worse</title><link>https://www.nijho.lt/post/native-harness-tools/</link><pubDate>Sun, 11 Oct 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/native-harness-tools/</guid><description>&lt;p&gt;A few nights ago, lying in bed, I had what felt like a galaxy-brain idea.&#10;I dictated it into my watch so I would still remember it in the morning.&lt;/p&gt;&#10;&lt;p&gt;Every frontier model learns to solve coding tasks with tools through reinforcement learning (RL), and I assume that training happens inside its vendor&amp;rsquo;s own coding agent: Anthropic&amp;rsquo;s models in Claude Code, and OpenAI&amp;rsquo;s models in Codex.&#10;That is a big assumption, since the labs do not publish their training environments, but it makes business sense: they sell those agents, so they have every reason to make their models work best in them.&#10;Every other harness, like &lt;a href="https://github.com/badlogic/pi-mono" target="_blank" rel="noopener"&gt;pi&lt;/a&gt;, &lt;a href="https://opencode.ai/" target="_blank" rel="noopener"&gt;opencode&lt;/a&gt;, or my own &lt;a href="https://www.nijho.lt/post/mindroom/"&gt;MindRoom&lt;/a&gt;, gives the model a set of tools of its own design for running commands and editing files.&#10;The models are usually smart enough to figure those out, but not always.&#10;Sometimes a model assumes an edit tool works like the one it was trained with, and uses it wrong.&#10;This summer, that happened to Claude Opus 4.8 in pi, which I come back to &lt;a href="#why-i-believed-it"&gt;below&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;So the idea was simple: in MindRoom, give each model the same shell and file-editing tools as its native harness, and switch them automatically depending on which model is answering.&#10;Claude would see Claude Code&amp;rsquo;s &lt;code&gt;Bash&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, and &lt;code&gt;Write&lt;/code&gt;.&#10;GPT would see Codex&amp;rsquo;s &lt;code&gt;exec_command&lt;/code&gt; and &lt;code&gt;apply_patch&lt;/code&gt;.&#10;And because the smallest agents do surprisingly well, I would go one step further: a minimal agent whose only tool is the shell each model knows from its training.&lt;/p&gt;&#10;&lt;p&gt;I was beyond excited.&#10;I could not really share that excitement locally, because my wife did not care, so I am sharing it with the internet instead.&lt;/p&gt;&#10;&lt;p&gt;Then I measured it against MindRoom&amp;rsquo;s own tools, and the familiar ones did not make the models more efficient.&#10;With only Codex&amp;rsquo;s tools, GPT-6 Astra did 84% more work, taking more steps and reading and writing more along the way, and used 59% more tokens.&#10;With Claude Code&amp;rsquo;s tools, Claude Sonnet 5.5 did 15% more work, and Claude only came out a few tokens cheaper because the descriptions I wrote for those tools are slightly shorter than MindRoom&amp;rsquo;s.&#10;Only one model clearly worked more efficiently with its own tools: Claude Opus 4.8, the model from the pi story.&#10;The models seem to generalize beyond the environment they were trained in, and what still matters is how much the agent sends with every request.&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;What I found&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Compared with MindRoom&amp;rsquo;s own tools, the familiar ones did not make the models more efficient: with only Codex&amp;rsquo;s tools, GPT-6 Astra and Sol did 60 to 84% more work and used 43 to 59% more tokens.&lt;/li&gt;&#10;&lt;li&gt;With Claude Code&amp;rsquo;s tools, Claude Sonnet 5.5 did 15% more work and Opus 5.5 about the same; they only came out a few tokens cheaper because the descriptions I wrote for those tools are shorter than MindRoom&amp;rsquo;s.&lt;/li&gt;&#10;&lt;li&gt;Only Claude Opus 4.8, the model whose edits kept failing in pi, clearly worked more efficiently with its own tools.&lt;/li&gt;&#10;&lt;li&gt;What matters is how much the agent sends with every request: a minimal agent with one shell tool and a 12-line prompt used 58 to 76% fewer tokens than the same agent in standard mode on these short tasks.&lt;/li&gt;&#10;&lt;li&gt;Giving that minimal agent each model&amp;rsquo;s own shell tool did not make it better.&lt;/li&gt;&#10;&lt;/ul&gt;&lt;/div&gt;&lt;/aside&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="#what-a-harness-is"&gt;What a harness is&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-i-believed-it"&gt;Why I believed it&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#smaller-agents-do-better"&gt;Smaller agents do better&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#models-should-know-their-own-tools-best"&gt;Models should know their own tools best&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-i-built"&gt;What I built&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#how-i-measured-it"&gt;How I measured it&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#native-tools-dont-make-todays-models-more-efficient"&gt;Native tools don&amp;rsquo;t make today&amp;rsquo;s models more efficient&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#older-models-only-opus-48-did-better-with-them"&gt;Older models: only Opus 4.8 did better with them&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-does-matter-what-the-agent-sends"&gt;What does matter: what the agent sends&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#fewer-tools"&gt;Fewer tools&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#minimal-mode-one-shell-tool-and-a-12-line-prompt"&gt;Minimal mode: one shell tool and a 12-line prompt&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#minimal-mode-with-each-models-native-shell-tool"&gt;Minimal mode with each model&amp;rsquo;s native shell tool&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-this-does-and-does-not-show"&gt;What this does and does not show&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-happens-to-the-pull-request"&gt;What happens to the pull request&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-this-excites-me"&gt;Why this excites me&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#appendix-how-i-measured-and-the-numbers-behind-the-charts"&gt;Appendix: how I measured, and the numbers behind the charts&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="what-a-harness-is"&gt;What a harness is &lt;a class="anchor" href="#what-a-harness-is" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&amp;ldquo;Harness&amp;rdquo; gets thrown around a lot, but there is no magic in it.&#10;Every coding agent talks to the model the same way: each request contains a system prompt, the definition of every tool the model may call, and the conversation so far.&#10;When the model answers with a tool call, the harness runs the tool, adds the result to the conversation, and sends everything again.&lt;/p&gt;&#10;&lt;figure class="harness-diagram" style="max-width: 460px; margin: 2rem auto;"&gt;&#10;&lt;svg viewBox="0 0 380 470" role="img" aria-label="One request holds the system prompt, the tool definitions, and the conversation so far. The model answers with a tool call, the harness runs it, adds the result to the conversation, and sends the whole request again." style="width: 100%; height: auto; font-family: system-ui, sans-serif;"&gt;&#10;&lt;defs&gt;&lt;marker id="harness-arrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse"&gt;&lt;path d="M0,0 L8,4 L0,8 z" fill="currentColor"/&gt;&lt;/marker&gt;&lt;/defs&gt;&#10;&lt;rect x="10" y="10" width="250" height="210" rx="10" fill="none" stroke="currentColor" stroke-opacity="0.45"/&gt;&#10;&lt;text x="22" y="32" font-size="14" font-weight="700" fill="currentColor"&gt;One request to the model&lt;/text&gt;&#10;&lt;rect x="22" y="44" width="226" height="48" rx="6" fill="#97bbf5" fill-opacity="0.45"/&gt;&#10;&lt;text x="34" y="64" font-size="13" font-weight="600" fill="currentColor"&gt;System prompt&lt;/text&gt;&#10;&lt;text x="34" y="81" font-size="11" fill="currentColor" fill-opacity="0.8"&gt;who the agent is, how it should work&lt;/text&gt;&#10;&lt;rect x="22" y="100" width="226" height="48" rx="6" fill="#97bbf5" fill-opacity="0.45"/&gt;&#10;&lt;text x="34" y="120" font-size="13" font-weight="600" fill="currentColor"&gt;Tool definitions&lt;/text&gt;&#10;&lt;text x="34" y="137" font-size="11" fill="currentColor" fill-opacity="0.8"&gt;name, description, and arguments&lt;/text&gt;&#10;&lt;rect x="22" y="156" width="226" height="52" rx="6" fill="#4269d0" fill-opacity="0.35"/&gt;&#10;&lt;text x="34" y="177" font-size="13" font-weight="600" fill="currentColor"&gt;Conversation so far&lt;/text&gt;&#10;&lt;text x="34" y="194" font-size="11" fill="currentColor" fill-opacity="0.8"&gt;the task, every tool call, every result&lt;/text&gt;&#10;&lt;path d="M268 46 h6 v100 h-6" fill="none" stroke="currentColor" stroke-opacity="0.6"/&gt;&#10;&lt;text x="281" y="88" font-size="11" fill="currentColor"&gt;the same on&lt;/text&gt;&#10;&lt;text x="281" y="102" font-size="11" fill="currentColor"&gt;every request&lt;/text&gt;&#10;&lt;path d="M268 158 h6 v48 h-6" fill="none" stroke="currentColor" stroke-opacity="0.6"/&gt;&#10;&lt;text x="281" y="170" font-size="11" fill="currentColor"&gt;grows with&lt;/text&gt;&#10;&lt;text x="281" y="184" font-size="11" fill="currentColor"&gt;every step&lt;/text&gt;&#10;&lt;line x1="135" y1="220" x2="135" y2="246" stroke="currentColor" marker-end="url(#harness-arrow)"/&gt;&#10;&lt;rect x="85" y="250" width="100" height="40" rx="20" fill="currentColor" fill-opacity="0.1" stroke="currentColor" stroke-opacity="0.45"/&gt;&#10;&lt;text x="135" y="275" font-size="13" font-weight="700" text-anchor="middle" fill="currentColor"&gt;Model&lt;/text&gt;&#10;&lt;line x1="186" y1="270" x2="214" y2="270" stroke="currentColor" stroke-opacity="0.6" marker-end="url(#harness-arrow)"/&gt;&#10;&lt;text x="220" y="274" font-size="11" fill="currentColor" fill-opacity="0.8"&gt;or a final answer&lt;/text&gt;&#10;&lt;line x1="135" y1="290" x2="135" y2="318" stroke="currentColor" marker-end="url(#harness-arrow)"/&gt;&#10;&lt;rect x="35" y="322" width="200" height="46" rx="6" fill="none" stroke="currentColor" stroke-opacity="0.45"/&gt;&#10;&lt;text x="47" y="341" font-size="13" font-weight="600" fill="currentColor"&gt;A tool call&lt;/text&gt;&#10;&lt;text x="47" y="358" font-size="11" fill="currentColor" fill-opacity="0.8" font-family="ui-monospace, monospace"&gt;Bash: ls data/&lt;/text&gt;&#10;&lt;line x1="135" y1="368" x2="135" y2="394" stroke="currentColor" marker-end="url(#harness-arrow)"/&gt;&#10;&lt;rect x="35" y="398" width="200" height="46" rx="6" fill="none" stroke="currentColor" stroke-opacity="0.45"/&gt;&#10;&lt;text x="47" y="417" font-size="13" font-weight="600" fill="currentColor"&gt;The harness runs it&lt;/text&gt;&#10;&lt;text x="47" y="434" font-size="11" fill="currentColor" fill-opacity="0.8"&gt;and adds the result&lt;/text&gt;&#10;&lt;path d="M235 421 H355 V199 H264" fill="none" stroke="currentColor" stroke-dasharray="4 3" marker-end="url(#harness-arrow)"/&gt;&#10;&lt;text x="347" y="300" font-size="11" fill="currentColor" text-anchor="middle" transform="rotate(-90 347 300)"&gt;sent again, all of it&lt;/text&gt;&#10;&lt;/svg&gt;&#10;&lt;figcaption&gt;What a coding agent sends with every step. The light parts are the same every time; only the conversation grows.&lt;/figcaption&gt;&#10;&lt;/figure&gt;&#10;&lt;p&gt;So from the model&amp;rsquo;s side, a harness is three things: its system prompt, its tool definitions, and what comes back when a tool runs.&#10;The first two are just text, so they are easy to copy, and the tool definitions are effectively the harness&amp;rsquo;s API: change them, and the model sees a different harness.&#10;The third is code, which takes more work to copy.&lt;/p&gt;&#10;&lt;p&gt;Because every request resends everything, a task&amp;rsquo;s tokens come in two parts.&#10;The fixed part is what the agent sends before anything happens: its system prompt, its tool definitions, and the task, which every request resends.&#10;The work is everything the steps add on top: the model&amp;rsquo;s replies, the tool results, and the growing conversation that every later request resends too.&#10;When I say below that a model did more work, I mean that second part.&lt;/p&gt;&#10;&lt;h2 id="why-i-believed-it"&gt;Why I believed it &lt;a class="anchor" href="#why-i-believed-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Two things convinced me: how well the smallest agents do, and what goes wrong when a model meets unfamiliar tools.&lt;/p&gt;&#10;&lt;h3 id="smaller-agents-do-better"&gt;Smaller agents do better &lt;a class="anchor" href="#smaller-agents-do-better" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Pi made its name by being small.&#10;When Mario Zechner &lt;a href="https://mariozechner.at/posts/2025-11-30-pi-coding-agent/" target="_blank" rel="noopener"&gt;introduced pi&lt;/a&gt; last November, its system prompt and tool definitions together came in below 1,000 tokens, and it had four tools: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, and &lt;code&gt;bash&lt;/code&gt;.&#10;He also wrote that &amp;ldquo;pi does not and will not support MCP&amp;rdquo;, the Model Context Protocol through which most agents load outside tools.&#10;And he pointed to Terminus 2, a minimal agent from the Terminal-Bench team, which was &amp;ldquo;holding its own against agents with far more sophisticated tooling&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;In August, DeepSeek released &lt;a href="https://github.com/deepseek-ai/deepseek-harness" target="_blank" rel="noopener"&gt;DeepSeek Harness&lt;/a&gt;, which was &lt;a href="https://news.ycombinator.com/item?id=49285244" target="_blank" rel="noopener"&gt;all over Hacker News&lt;/a&gt; and the local AI subreddits I read.&#10;Its standard setup comes with a full set of tools.&#10;But for benchmarks, DeepSeek &lt;a href="https://github.com/deepseek-ai/deepseek-harness/blob/d743267388641bc76f17c45ce8b4c231aed1d32c/BENCHMARK.md" target="_blank" rel="noopener"&gt;points to its minimal profile&lt;/a&gt;.&#10;In early September, that profile had &lt;a href="https://github.com/deepseek-ai/deepseek-harness/blob/63795eaa5cba78cc0a92d95fccd8db5523d7f050/packages/bundle/sdk-minimal/README.md#L78-L84" target="_blank" rel="noopener"&gt;exactly one tool, a persistent &lt;code&gt;bash&lt;/code&gt;, and a one-line system prompt&lt;/a&gt;: &amp;ldquo;You are a helpful software engineer assistant.&amp;rdquo;&#10;(Ten days later, it already had a second tool, &lt;code&gt;working_directory&lt;/code&gt;; staying that small seems to be hard.)&#10;That is what inspired the minimal mode I added to MindRoom in September, which gives an agent a short prompt and a single &lt;code&gt;bash&lt;/code&gt; tool.&lt;/p&gt;&#10;&lt;h3 id="models-should-know-their-own-tools-best"&gt;Models should know their own tools best &lt;a class="anchor" href="#models-should-know-their-own-tools-best" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The case that got me thinking about this was Claude Opus 4.8 in pi.&#10;In July, a pi user reported that &lt;a href="https://github.com/earendil-works/pi/issues/6278" target="_blank" rel="noopener"&gt;about 20% of its edits failed in some sessions&lt;/a&gt;.&#10;Pi&amp;rsquo;s &lt;code&gt;edit&lt;/code&gt; tool takes a list of replacements, and Opus 4.8 kept adding made-up fields to them, like &lt;code&gt;in_file&lt;/code&gt;, &lt;code&gt;matchCase&lt;/code&gt;, or &lt;code&gt;newText2&lt;/code&gt;.&#10;Claude Sonnet 5 did it too, while Opus 4.7 and older Claude models never did in the same tests.&lt;/p&gt;&#10;&lt;p&gt;Armin Ronacher, one of pi&amp;rsquo;s maintainers, &lt;a href="https://github.com/earendil-works/pi/issues/6278#issuecomment-4883362982" target="_blank" rel="noopener"&gt;dug into it&lt;/a&gt; and compared it with what Claude Code does with the tool calls it receives.&#10;It turns out that Claude Code quietly repairs a lot of them:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;It accepts several names for the same argument, like &lt;code&gt;old_str&lt;/code&gt; and &lt;code&gt;old_string&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;It parses an argument that arrives as a string when it should be an object.&lt;/li&gt;&#10;&lt;li&gt;It fixes broken &lt;code&gt;\uXXXX&lt;/code&gt; escapes in strings.&lt;/li&gt;&#10;&lt;li&gt;It drops fields it does not know.&lt;/li&gt;&#10;&lt;li&gt;When it cannot parse a call at all, it asks the model to try again.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;His hypothesis is that RL itself might be the cause.&#10;If a model is trained in a harness that absorbs these mistakes, a slightly wrong tool call still completes the task and still gets rewarded, so nothing teaches the model not to make it.&#10;Pi &lt;a href="https://github.com/earendil-works/pi/commit/a1b336d73e13b53949ff629800081185d3e4694e" target="_blank" rel="noopener"&gt;now ignores unknown fields too&lt;/a&gt;, just like Claude Code.&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;Then pi itself strayed from its minimal roots.&#10;Pi 1.0 came out on October 1 and &lt;a href="https://earendil.com/posts/pi-1-0/" target="_blank" rel="noopener"&gt;added MCP after all&lt;/a&gt;.&#10;&lt;a href="https://www.theregister.com/ai-and-ml/2026/10/02/pi-coding-agent-pulls-a-180-and-adds-mcp-support/5300678" target="_blank" rel="noopener"&gt;The Register&lt;/a&gt; called it a 180.&#10;On &lt;a href="https://news.ycombinator.com/item?id=49926069" target="_blank" rel="noopener"&gt;Hacker News&lt;/a&gt; and &lt;a href="https://www.reddit.com/r/LocalLLaMA/comments/1wvffcr/pi_10_released_mcp_support_now_included_by_default/" target="_blank" rel="noopener"&gt;r/LocalLLaMA&lt;/a&gt;, people worried that &lt;a href="https://news.ycombinator.com/item?id=49926549" target="_blank" rel="noopener"&gt;&amp;ldquo;Pi&amp;rsquo;s days as a nice minimal agent TUI are numbered&amp;rdquo;&lt;/a&gt;.&#10;What struck me was how the maintainers explained it.&#10;&lt;a href="https://news.ycombinator.com/item?id=49926377" target="_blank" rel="noopener"&gt;Armin Ronacher&lt;/a&gt; wrote that the models &amp;ldquo;are trained on their respective harnesses and we&amp;rsquo;re not here to fight their behavior&amp;rdquo;, and &lt;a href="https://news.ycombinator.com/item?id=49926840" target="_blank" rel="noopener"&gt;Mario Zechner&lt;/a&gt; that &amp;ldquo;we follow what the models are trained on&amp;rdquo;.&#10;That is exactly the native-tools half of my plan.&lt;/p&gt;&#10;&lt;p&gt;My plan combined both: a minimal agent, with each model&amp;rsquo;s native shell as its only tool.&#10;Terminal-Bench also said something I chose to ignore: agents like Terminus 2 do well with tools no model was trained on.&#10;I did not believe that part.&#10;In opencode and pi, I had repeatedly seen models get the edit tool wrong, so I assumed the benchmarks did not carry over to real work.&#10;My belief that a model works best in the harness it was trained in was that strong: I expected the native tools to help, and to help most in a minimal agent with nothing but a shell.&lt;/p&gt;&#10;&lt;h2 id="what-i-built"&gt;What I built &lt;a class="anchor" href="#what-i-built" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;MindRoom is my open-source platform for AI agents that live in Matrix chat rooms, where several agents can work together.&#10;Its agents get shell and file tools under MindRoom&amp;rsquo;s own names, like &lt;code&gt;run_shell_command&lt;/code&gt;, &lt;code&gt;read_file&lt;/code&gt;, and &lt;code&gt;edit_file&lt;/code&gt;.&#10;In &lt;a href="https://github.com/mindroom-ai/mindroom/pull/2766" target="_blank" rel="noopener"&gt;this pull request&lt;/a&gt;, I added what I call tool dialects, which show a model those tools in the shape of the coding agent it was trained in.&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;Model&lt;/th&gt;&#10;&lt;th&gt;Sees&lt;/th&gt;&#10;&lt;th&gt;Instead of&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;Bash&lt;/code&gt;, &lt;code&gt;BashOutput&lt;/code&gt;, &lt;code&gt;KillShell&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Edit&lt;/code&gt;, and &lt;code&gt;Write&lt;/code&gt;, as in Claude Code&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;run_shell_command&lt;/code&gt;, &lt;code&gt;check_shell_command&lt;/code&gt;, &lt;code&gt;kill_shell_command&lt;/code&gt;, &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;edit_file&lt;/code&gt;, and &lt;code&gt;write_file&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;exec_command&lt;/code&gt;, &lt;code&gt;write_stdin&lt;/code&gt;, and a freeform &lt;code&gt;apply_patch&lt;/code&gt; that takes the patch as plain text, as in Codex, and a &lt;code&gt;kill_shell_command&lt;/code&gt; that takes Codex&amp;rsquo;s &lt;code&gt;session_id&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;code&gt;run_shell_command&lt;/code&gt;, &lt;code&gt;check_shell_command&lt;/code&gt;, &lt;code&gt;kill_shell_command&lt;/code&gt;, &lt;code&gt;edit_file&lt;/code&gt;, and &lt;code&gt;write_file&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;Inside MindRoom, nothing changes: approvals, hooks, and the stored history keep MindRoom&amp;rsquo;s names, and the translation only happens on the way to and from the model.&#10;That gives MindRoom a property I like a lot: a conversation can switch from Claude to GPT halfway through, and each model sees the earlier tool calls in its own shape, as if it had made them in its own harness.&lt;/p&gt;&#10;&lt;p&gt;Matching the native harness exactly turned out to be a stretch, though.&#10;Codex has no tool for reading files, so in the pull request, GPT keeps MindRoom&amp;rsquo;s &lt;code&gt;read_file&lt;/code&gt;, and both keep MindRoom&amp;rsquo;s &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find_files&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt;, which makes each native set a mix.&#10;For the main comparison, I removed those extra tools, so each model saw only the tools of its own harness.&#10;The tools have the same names and arguments as in Claude Code and Codex, but I wrote short descriptions of my own, because Claude Code&amp;rsquo;s are not openly licensed.&#10;What they return is MindRoom&amp;rsquo;s output, and the system prompt is MindRoom&amp;rsquo;s too.&#10;So keep in mind for everything below: I tested the names and shapes of the tools, not the whole harness.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;How the translation works&lt;/summary&gt;&#10; &lt;ul&gt;&#10;&lt;li&gt;Codex&amp;rsquo;s &lt;code&gt;apply_patch&lt;/code&gt; is freeform: the model writes the patch as plain text instead of JSON arguments. I ported Codex&amp;rsquo;s patch parser and the code that applies patches, together with Codex&amp;rsquo;s own test cases, so a patch from a Codex-trained model applies exactly as it would in Codex. Its description comes from Codex too, which is openly licensed.&lt;/li&gt;&#10;&lt;li&gt;A switched model sees earlier calls in its own shape only for the tools its own harness also has, like the shell: an edit made with Claude Code&amp;rsquo;s &lt;code&gt;Edit&lt;/code&gt; stays an &lt;code&gt;edit_file&lt;/code&gt; call for GPT, which has no matching tool.&lt;/li&gt;&#10;&lt;li&gt;An earlier version of the pull request also rewrote every tool result to look like the native one. That was a lot of fragile code, so I removed it before I measured anything.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;h2 id="how-i-measured-it"&gt;How I measured it &lt;a class="anchor" href="#how-i-measured-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I used 12 terminal tasks from an evaluation suite I built for MindRoom, like writing a report from a CSV file, finding the commit that broke a check, or fixing failing tests.&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;A hidden check graded each result, and every setup ran every task three times.&#10;Almost every run passed, so the interesting number is how many tokens the agent needed to get there, cached input included, and cheaper in what follows means fewer tokens.&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;&#10;The tool comparisons ran through MindRoom&amp;rsquo;s OpenAI-compatible API; minimal mode, which only works in chat, ran through Matrix.&#10;I tested five current models, Claude Opus 5.5 and Sonnet 5.5, and GPT-6 Astra, GPT-6.1 Sol, and GPT-6 Luna, and later &lt;a href="#older-models-only-opus-48-did-better-with-them"&gt;five older ones&lt;/a&gt;.&#10;The current Claude models ran with adaptive thinking, where the model decides when to think.&#10;I called the GPT models from MindRoom through OpenAI&amp;rsquo;s Codex backend, the endpoint the Codex CLI uses, so no run used Codex itself; with no reasoning effort set, the backend picks one.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;Details of the setup&lt;/summary&gt;&#10; &lt;ul&gt;&#10;&lt;li&gt;A small harness sent each task to MindRoom&amp;rsquo;s OpenAI-compatible API as a new conversation, and a hidden check graded the result in a separate container without network access. Every task ran three times per configuration, so each number in this post comes from 36 runs.&lt;/li&gt;&#10;&lt;li&gt;The agent&amp;rsquo;s commands ran in a Docker container, the way MindRoom runs agents it does not fully trust.&lt;/li&gt;&#10;&lt;li&gt;With adaptive thinking, the current Claude models thought before 15 to 25% of their replies, with MindRoom&amp;rsquo;s tools and with the native ones.&lt;/li&gt;&#10;&lt;li&gt;I ran the GPT models through OpenAI&amp;rsquo;s Codex backend, the endpoint the Codex CLI uses, and sent no reasoning effort, so the backend picked one. The GPT-6 models barely reasoned on these tasks: in those runs, only 4 to 12% of their requests had any reasoning tokens, and then 7 to 234 of them.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;h2 id="native-tools-dont-make-todays-models-more-efficient"&gt;Native tools don&amp;rsquo;t make today&amp;rsquo;s models more efficient &lt;a class="anchor" href="#native-tools-dont-make-todays-models-more-efficient" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;To make the comparison fair, I gave each model only the tools of its own harness, against MindRoom&amp;rsquo;s matching set.&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;For Claude, that is a shell and tools to read, edit, and write files; for GPT, a shell and Codex&amp;rsquo;s &lt;code&gt;apply_patch&lt;/code&gt;, against MindRoom&amp;rsquo;s tools to edit and write files.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task when each model uses its native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals: with only the harness&amp;rsquo;s own tools against MindRoom&amp;rsquo;s matching set, and with the mixed set in the pull request. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="fairCurrent"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task when each model uses its native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals: with only the harness&amp;rsquo;s own tools against MindRoom&amp;rsquo;s matching set, and with the mixed set in the pull request. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;Claude got 6 to 13% cheaper with Claude Code&amp;rsquo;s tools, but only because the definitions I wrote for them are about 480 tokens shorter than MindRoom&amp;rsquo;s, and every request resends them; the tools themselves saved nothing, and Sonnet 5.5 even did 15% more work with them.&#10;It never even touched the file tools: Opus 5.5 and Sonnet 5.5 did everything in the shell, with either set.&lt;/p&gt;&#10;&lt;p&gt;GPT-6 Astra and Sol got much more expensive, while Luna barely changed.&#10;With only Codex&amp;rsquo;s tools, GPT-6 Astra did 84% more work and used 59% more tokens.&#10;It called &lt;code&gt;apply_patch&lt;/code&gt; 30 times, as a separate step for edits it otherwise made inside its shell commands, and every extra step resends the whole conversation so far.&#10;I expected the opposite: these are the tools I assumed these models were trained on, so I thought they would need fewer and more confident steps.&#10;The mixed set, the lighter rows, points the same way.&lt;/p&gt;&#10;&lt;p&gt;The tool mistakes I wanted to prevent did not happen with MindRoom&amp;rsquo;s tools at all: with its matching set, none of the five models made a single tool error.&#10;The only errors came from the mixed set: next to Codex&amp;rsquo;s tools, Luna had to guess how MindRoom&amp;rsquo;s &lt;code&gt;read_file&lt;/code&gt; resolves paths, and got it wrong seven times.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;The numbers behind this section&lt;/summary&gt;&#10; &lt;p&gt;&lt;strong&gt;Fixed part and work.&lt;/strong&gt;&#10;Every request to a model sends the whole conversation so far, plus the definition of every tool the model can use.&#10;So I split each task&amp;rsquo;s tokens in two.&#10;The fixed part is the first request&amp;rsquo;s input, which holds the system prompt, the tool definitions, and the task, times the number of requests.&#10;The work is everything else: tool results and the model&amp;rsquo;s replies, counted again with every later request that resends them, so it also grows with the number of steps.&#10;Providers cache most of the fixed part between requests and charge much less for cached input, so it costs less than its share of the tokens suggests.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Only the harness&amp;rsquo;s own tools.&lt;/strong&gt;&#10;With Claude Code&amp;rsquo;s tools, Opus 5.5 used 13% fewer tokens [95% CI −19 to −6%] and Sonnet 5.5 6% fewer [−10 to −1%].&#10;Both came from the definitions: the Claude Code-style set I wrote is about 480 tokens shorter, while the work on top of it grew by 7% [−7 to +23%] and 15% [+7 to +25%].&#10;With Codex&amp;rsquo;s tools, GPT-6 Astra used 59% more tokens [+53 to +65%] and GPT-6.1 Sol 43% more [+33 to +52%].&#10;They called &lt;code&gt;apply_patch&lt;/code&gt; 30 and 22 times, where with MindRoom&amp;rsquo;s tools they made only 3 and 6 calls to the file tools, so they needed 4.7 and 4.4 requests per task instead of 3.5 and 3.6 and did 84% [+73 to +96%] and 60% [+44 to +76%] more work.&#10;GPT-6 Luna used about the same with either set, +3% [−8 to +15%], and called &lt;code&gt;apply_patch&lt;/code&gt; only 8 times.&#10;The pass rates stayed within one or two runs of each other, and no model made a tool error with either set.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The mixed set, as it is in the pull request.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task when each model uses its native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals. The second row caps the native shell output at the last 100 lines, MindRoom&amp;rsquo;s shell default. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="nativeDelta"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task when each model uses its native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals. The second row caps the native shell output at the last 100 lines, MindRoom&amp;rsquo;s shell default. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;p&gt;With Claude Code&amp;rsquo;s tools, both Claude models used about 9% fewer tokens.&#10;With Codex&amp;rsquo;s tools, GPT-6 Astra used 31% more tokens and GPT-6.1 Sol 19% more.&#10;An earlier run of the same experiment, before some unrelated fixes in the pull request, gave almost the same numbers: 33% more for Astra and 15% more for Sol.&#10;For GPT-6 Luna, the difference was too small to tell apart from noise.&#10;The pass rates barely moved: Opus, Sonnet, and Sol passed all 36 runs with either set of tools, and Astra failed one run in some configurations.&#10;Luna passed 33 runs with MindRoom&amp;rsquo;s tools and 35 with Codex&amp;rsquo;s; in the earlier run it was the other way around, 35 against 32, so I would not read anything into it either.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;It is not the longer output.&lt;/strong&gt;&#10;MindRoom&amp;rsquo;s own shell tool returns the last 100 lines of a command&amp;rsquo;s output by default.&#10;For the native tools, I returned everything up to 50 KiB, because that is closer to what Claude Code and Codex do.&#10;So I ran everything again with the native tools capped at the same 100 lines, the second row in the chart above.&#10;The cap barely changes anything: Astra still uses 26% more tokens, Sol 15% more, and the Claude models still save about 8%.&#10;The models hardly ever print more than 100 lines anyway: in the uncapped run, 3 of Astra&amp;rsquo;s 91 shell results and 3 of Sol&amp;rsquo;s 93 were longer than that, and none of Claude&amp;rsquo;s 220.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Where the tokens went with the mixed set.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Tokens per task with MindRoom&amp;rsquo;s shell and file tools and with the native ones, split into the fixed part (light) and the work the model did on top of it (dark). Hover for the number of requests." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="tokenSplit"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Tokens per task with MindRoom&amp;rsquo;s shell and file tools and with the native ones, split into the fixed part (light) and the work the model did on top of it (dark). Hover for the number of requests.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;p&gt;Claude did not work any differently with Claude Code&amp;rsquo;s tools.&#10;It never called a file tool with either set, did everything in the shell, and made about the same number of requests.&#10;Its work changed by 5% or less, and almost all of its savings come from the fixed part: the Claude Code-style definitions I wrote are about 480 tokens shorter than MindRoom&amp;rsquo;s.&lt;/p&gt;&#10;&lt;p&gt;GPT did work differently.&#10;With Codex&amp;rsquo;s tools, Astra and Sol edited files with &lt;code&gt;apply_patch&lt;/code&gt; 17 and 15 times, where with MindRoom&amp;rsquo;s tools they made only 3 and 5 calls to the file tools and did the rest from the shell.&#10;Each patch is a request of its own, which accounts for their 0.3 to 0.5 extra requests per task.&#10;They also did 51% and 24% more work.&#10;And Codex&amp;rsquo;s tools take about 130 more tokens to describe than MindRoom&amp;rsquo;s, because &lt;code&gt;apply_patch&lt;/code&gt; costs about 270 more than &lt;code&gt;edit_file&lt;/code&gt; and &lt;code&gt;write_file&lt;/code&gt; together, so the fixed part grew as well.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The mistakes.&lt;/strong&gt;&#10;None of the five models made a single tool error with MindRoom&amp;rsquo;s tools in its 36 runs.&#10;Partly, the models rarely needed the file tools: the Claude models never called one, and of the GPT models, only Luna used them often.&#10;The only model that made tool errors was Luna, and only with the Codex tools: 7 errors in 36 runs, all of them &lt;code&gt;read_file&lt;/code&gt; calls with a path relative to the directory Luna had just given &lt;code&gt;exec_command&lt;/code&gt;.&#10;The made-up fields in pi always appeared inside its nested list of replacements, where the model has to write the JSON for each replacement itself.&#10;MindRoom&amp;rsquo;s &lt;code&gt;edit_file&lt;/code&gt; is flat, like Claude Code&amp;rsquo;s &lt;code&gt;Edit&lt;/code&gt;: a path, the old text, and the new text.&#10;So I suspected that keeping the tools as simple as the native ones matters more than matching their exact names.&lt;/p&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;h2 id="older-models-only-opus-48-did-better-with-them"&gt;Older models: only Opus 4.8 did better with them &lt;a class="anchor" href="#older-models-only-opus-48-did-better-with-them" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The pi story made me wonder whether this used to be different, since Opus 4.8 struggled with pi&amp;rsquo;s edit tool while Opus 4.7 did not.&#10;So I ran the same comparisons on older models that are still available: Claude Opus 4.7, Opus 4.8, and Sonnet 5, and GPT-5.5 and GPT-5.6 Sol.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task when the older models use their native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals: with only the harness&amp;rsquo;s own tools against MindRoom&amp;rsquo;s matching set, and with the mixed set in the pull request. Opus 4.7 and 4.8 ran without thinking, Sonnet 5 with adaptive thinking, and GPT with the effort Codex picks. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="fairOlder"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task when the older models use their native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals: with only the harness&amp;rsquo;s own tools against MindRoom&amp;rsquo;s matching set, and with the mixed set in the pull request. Opus 4.7 and 4.8 ran without thinking, Sonnet 5 with adaptive thinking, and GPT with the effort Codex picks. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;With only each harness&amp;rsquo;s own tools, the older models did not prefer their native tools either, except for one.&#10;The exception is the model from the pi story: Opus 4.8 did 17% less work and used 19% fewer tokens with Claude Code&amp;rsquo;s tools, reading files through Bash instead of with a separate tool.&#10;With the mixed set, the older Claude models seemed to prefer Claude Code&amp;rsquo;s tools, using 22 to 36% fewer tokens, but that came from the extra tools the mixed set keeps from MindRoom, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find_files&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt;: next to MindRoom&amp;rsquo;s other tools, those made the older Claude models take more steps and read more, and next to Claude Code&amp;rsquo;s tools, the models hardly touched them.&#10;And on the pi story itself: Opus 4.8 and Sonnet 5, the two models that made up fields in pi, made 111 edits with MindRoom&amp;rsquo;s &lt;code&gt;edit_file&lt;/code&gt;, which takes a single path, old text, and new text, and none of them failed.&lt;/p&gt;&#10;&lt;p&gt;With only each harness&amp;rsquo;s own tools, the GPT-5 models did about the same with either set, and unlike GPT-6 Astra and Sol, they barely used &lt;code&gt;apply_patch&lt;/code&gt;.&#10;So reaching for its native editing tool, and paying for it, is new in that generation.&lt;/p&gt;&#10;&lt;p&gt;Of the ten models I tested, familiar tools clearly made only one more efficient, the one that started it.&#10;The others did as well or better with tools they were presumably not trained on: they seem to generalize beyond the environment they were trained in.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;The numbers behind this section&lt;/summary&gt;&#10; &lt;p&gt;&lt;strong&gt;Only the harness&amp;rsquo;s own tools.&lt;/strong&gt;&#10;With Claude Code&amp;rsquo;s tools, Opus 4.7 used 3% fewer tokens [−12 to +6%], Opus 4.8 19% fewer [−26 to −11%], and Sonnet 5 7% fewer [−29 to +23%], a noisy result.&#10;Opus 4.8 did 17% less work [−29 to −4%] on top of the shorter definitions, while the other older models&amp;rsquo; work changed by −5 to +13%, none of it clearly; Opus 4.8 called MindRoom&amp;rsquo;s &lt;code&gt;read_file&lt;/code&gt; 27 times, but never Claude Code&amp;rsquo;s &lt;code&gt;Read&lt;/code&gt;.&#10;With Codex&amp;rsquo;s tools, GPT-5.5 used 5% more [−5 to +16%] and GPT-5.6 Sol 3% more [−4 to +11%]; they called &lt;code&gt;apply_patch&lt;/code&gt; only 3 and 9 times, where with MindRoom&amp;rsquo;s tools they made 20 and 16 calls to the file tools.&#10;The pass rates stayed within one or two runs of each other.&#10;None of the older models made a tool error with MindRoom&amp;rsquo;s matching set; with Codex&amp;rsquo;s, GPT-5.5 sent garbled working directories to &lt;code&gt;exec_command&lt;/code&gt; 11 times.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Why the mixed set made them look different.&lt;/strong&gt;&#10;The only difference between the two tests is that the mixed set adds MindRoom&amp;rsquo;s &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find_files&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt; to both sides.&#10;Next to MindRoom&amp;rsquo;s other tools, the older Claude models called &lt;code&gt;ls&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; 20, 9, and 3 times, took 0.3 to 0.9 more requests per task, and did 35 to 38% more work than without them.&#10;Next to Claude Code&amp;rsquo;s tools, they called them once in total.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The older models with the mixed set.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task when each model uses its native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals, oldest models first in each family. Each model&amp;rsquo;s reasoning mode is in parentheses: Opus 4.7 and 4.8 do not think, the other Claude models think adaptively, and GPT uses the effort Codex picks. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="generations"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task when each model uses its native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals, oldest models first in each family. Each model&amp;rsquo;s reasoning mode is in parentheses: Opus 4.7 and 4.8 do not think, the other Claude models think adaptively, and GPT uses the effort Codex picks. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;p&gt;Opus 4.7, Opus 4.8, and Sonnet 5 used 22 to 36% fewer tokens with Claude Code&amp;rsquo;s tools.&#10;Unlike the 5.5 models, they also made fewer requests and did 26 to 45% less work on top of the fixed part.&#10;GPT-5.6 Sol used 15% fewer tokens with Codex&amp;rsquo;s tools and did 27% less work, and GPT-5.5 used about the same.&#10;One generation later, the GPT-6 models use up to 31% more with Codex&amp;rsquo;s tools, and the Claude 5.5 models mostly save what the shorter definitions save.&#10;The pass rates stayed within one or two runs of each other for every model.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;How they used the file tools.&lt;/strong&gt;&#10;With MindRoom&amp;rsquo;s tools, the older Claude models called &lt;code&gt;read_file&lt;/code&gt; 24, 29, and 44 times; with Claude Code&amp;rsquo;s, they called &lt;code&gt;Read&lt;/code&gt; only 3, 1, and 6 times and read files through Bash instead.&#10;GPT-5.6 Sol needed 9 patches where it had made 32 calls to &lt;code&gt;edit_file&lt;/code&gt; and &lt;code&gt;write_file&lt;/code&gt;.&#10;Opus 4.8 and Sonnet 5 made 37 and 21 calls to MindRoom&amp;rsquo;s flat &lt;code&gt;edit_file&lt;/code&gt;, and none of them failed.&#10;That does not prove that pi&amp;rsquo;s nested list caused pi&amp;rsquo;s failures, because I never gave them a nested edit tool, but a flat one did not trigger them.&#10;Their few tool errors were different ones: Sonnet 5 twice polled a background command that did not exist, GPT-5.5 and GPT-5.6 Sol each had one edit whose old text did not match, and GPT-5.5 sent garbled working directories to Codex&amp;rsquo;s &lt;code&gt;exec_command&lt;/code&gt; seven times.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Only the shell tools.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task when the older models use their native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals: with shell and file tools, and with only the shell tools. Opus 4.7 and 4.8 ran without thinking, Sonnet 5 with adaptive thinking, and GPT with the effort Codex picks. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="olderShell"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task when the older models use their native harness tools instead of MindRoom&amp;rsquo;s, with 95% confidence intervals: with shell and file tools, and with only the shell tools. Opus 4.7 and 4.8 ran without thinking, Sonnet 5 with adaptive thinking, and GPT with the effort Codex picks. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;p&gt;The native set mixes a model&amp;rsquo;s own tools with some of MindRoom&amp;rsquo;s, such as &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, and, for GPT, &lt;code&gt;read_file&lt;/code&gt;, so I also ran the older models with only the shell tools, in MindRoom&amp;rsquo;s shape and in the native one.&#10;Opus 4.7 then used 10% fewer tokens with the native shell, GPT-5.6 Sol 6% fewer, and GPT-5.5 the same, and almost all of that came from shorter definitions.&#10;Opus 4.8 used 23% fewer tokens, made fewer requests, and did 16% less work.&#10;Sonnet 5&amp;rsquo;s result is too noisy to read: in one run, the native shell returned a 50 KB output early on, and resending it with every later request made that run cost 12 times the median; its median run used 9% fewer tokens with the native shell.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The reasoning settings.&lt;/strong&gt;&#10;Opus 4.7 and 4.8 do not think unless you ask them to, and Sonnet 5 thought before 42% of its replies, against 15 to 25% for the 5.5 models at the same adaptive setting.&#10;The GPT-5 models reasoned before 59 to 76% of their requests, while the GPT-6 models reasoned before only 4 to 12%.&#10;With adaptive thinking and the mixed set, Opus 4.7 and 4.8 thought before 26 to 36% of their replies, and still used 35% and 20% fewer tokens with Claude Code&amp;rsquo;s tools.&#10;For that test, I set &lt;code&gt;thinking&lt;/code&gt; to &lt;code&gt;adaptive&lt;/code&gt; in the model&amp;rsquo;s &lt;code&gt;extra_kwargs&lt;/code&gt;, since these models reject a fixed thinking budget.&lt;/p&gt;&#10;&lt;p&gt;I also ran one current model of each family, Claude Sonnet 5.5 and GPT-6 Astra, at low and at high reasoning effort.&#10;For Astra, I set &lt;code&gt;reasoning_effort&lt;/code&gt; to &lt;code&gt;low&lt;/code&gt; or &lt;code&gt;high&lt;/code&gt;; for Sonnet 5.5, &lt;code&gt;output_config.effort&lt;/code&gt;, which is how the Claude 5.5 models take a reasoning effort.&lt;/p&gt;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task when Claude Sonnet 5.5 and GPT-6 Astra use their native harness tools instead of MindRoom&amp;rsquo;s, at each reasoning effort, with 95% confidence intervals. With no effort set, Sonnet uses adaptive thinking, and Astra the effort Codex picks. Hover for the tokens per task." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="effortChange"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task when Claude Sonnet 5.5 and GPT-6 Astra use their native harness tools instead of MindRoom&amp;rsquo;s, at each reasoning effort, with 95% confidence intervals. With no effort set, Sonnet uses adaptive thinking, and Astra the effort Codex picks. Hover for the tokens per task.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;p&gt;Both models passed all 36 runs at every effort with either set of tools, except for one run that Astra failed with MindRoom&amp;rsquo;s tools at its default effort.&#10;A higher effort made Codex&amp;rsquo;s tools worse for GPT. At high effort, Astra made 4.5 requests per task with Codex&amp;rsquo;s tools against 3.6 with MindRoom&amp;rsquo;s, and used 45% more tokens.&#10;Even at high effort, though, Astra reasoned before only 10 to 12% of its requests, against 4 to 7% at its default, far below the GPT-5 models.&#10;For Claude, the native tools stayed cheaper at every level, and saved the most at low effort, 19%, where Sonnet thought before only 3 to 4% of its replies.&#10;Sonnet thought about as often at its default as at high effort, so I would not read much into the difference between those two.&lt;/p&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;h2 id="what-does-matter-what-the-agent-sends"&gt;What does matter: what the agent sends &lt;a class="anchor" href="#what-does-matter-what-the-agent-sends" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My next guess was the other half of my plan, a smaller agent.&#10;Every request to the model resends the system prompt and the definition of every tool, so an agent with a shorter prompt and fewer tools sends less with every step.&lt;/p&gt;&#10;&lt;h3 id="fewer-tools"&gt;Fewer tools &lt;a class="anchor" href="#fewer-tools" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Fewer tools did help.&#10;With only MindRoom&amp;rsquo;s shell tools and none of its file tools, the agent used about a third fewer tokens and passed about as many runs.&#10;A shell-only agent is also where I still expected the native tools to matter most, because every step goes through the one tool whose shape the model knows best.&#10;They did not: GPT used the same number of tokens with Codex&amp;rsquo;s shell tools, and Claude saved only what its shorter descriptions save.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;The numbers behind this section&lt;/summary&gt;&#10; &lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task, with 95% confidence intervals: with only MindRoom&amp;rsquo;s shell tools instead of all of MindRoom&amp;rsquo;s tools, and with only the native shell tools instead of MindRoom&amp;rsquo;s. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="shellOnly"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task, with 95% confidence intervals: with only MindRoom&amp;rsquo;s shell tools instead of all of MindRoom&amp;rsquo;s tools, and with only the native shell tools instead of MindRoom&amp;rsquo;s. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;p&gt;With only MindRoom&amp;rsquo;s three shell tools, for running, checking, and stopping a command, and none of its file tools, the agent used 27 to 41% fewer tokens.&#10;It passed about as many runs: Luna passed 31 instead of 33, too small a difference to tell apart, and the other four models passed all 36.&lt;/p&gt;&#10;&lt;p&gt;For the native shape, I gave Claude only Claude Code&amp;rsquo;s &lt;code&gt;Bash&lt;/code&gt;, &lt;code&gt;BashOutput&lt;/code&gt;, and &lt;code&gt;KillShell&lt;/code&gt;, and GPT only Codex&amp;rsquo;s &lt;code&gt;exec_command&lt;/code&gt; and &lt;code&gt;write_stdin&lt;/code&gt;, with MindRoom&amp;rsquo;s &lt;code&gt;kill_shell_command&lt;/code&gt; in Codex&amp;rsquo;s style.&#10;For GPT, the total did not move: Astra, Sol, and Luna used the same number of tokens, within 2%, and made the same number of requests.&#10;Codex&amp;rsquo;s shell tools alone take about 130 fewer tokens to describe than MindRoom&amp;rsquo;s, and GPT spent about as much on extra work.&#10;Claude Sonnet 5.5 used 18% fewer tokens and Claude Opus 5.5 6% fewer, mostly because the Claude Code-style shell tools are shorter to describe than MindRoom&amp;rsquo;s.&lt;/p&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;h3 id="minimal-mode-one-shell-tool-and-a-12-line-prompt"&gt;Minimal mode: one shell tool and a 12-line prompt &lt;a class="anchor" href="#minimal-mode-one-shell-tool-and-a-12-line-prompt" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;MindRoom&amp;rsquo;s &lt;a href="https://docs.mindroom.chat/tools/agent-cli/" target="_blank" rel="noopener"&gt;minimal mode&lt;/a&gt;, modeled on DeepSeek&amp;rsquo;s minimal profile, takes this all the way.&#10;The agent gets a 12-line prompt and a single &lt;code&gt;bash&lt;/code&gt; tool instead of its normal prompt and tools.&#10;Its instructions, skills, context files, and memory move behind a command-line program inside that shell, which it can call when it needs them.&#10;Minimal mode only works in chat, so I ran this comparison in Matrix conversations, against the same agent in standard mode with only its shell tools.&#10;A chat adds context about the room to every request, but even so, the minimal agent used 21 to 52% fewer tokens than the shell-only agent through MindRoom&amp;rsquo;s API.&lt;/p&gt;&#10;&lt;p&gt;The difference starts before the model does anything.&#10;For Claude Sonnet 5.5, the prompt and tool definitions sent with every request shrink from 4,285 tokens to 661.&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;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="What every request carries besides the conversation: the system prompt and the tool definitions, in standard and in minimal mode, from each provider&amp;rsquo;s own token counts. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="promptParts"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;What every request carries besides the conversation: the system prompt and the tool definitions, in standard and in minimal mode, from each provider&amp;rsquo;s own token counts. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Tokens per task in Matrix conversations: the same agent with only MindRoom&amp;rsquo;s shell tools, in standard mode and in minimal mode with its single bash tool, split into the fixed part (light) and the work the model did on top of it (dark). Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="minimalSplit"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Tokens per task in Matrix conversations: the same agent with only MindRoom&amp;rsquo;s shell tools, in standard mode and in minimal mode with its single bash tool, split into the fixed part (light) and the work the model did on top of it (dark). Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;Each bar splits a task&amp;rsquo;s tokens into the fixed part, the prompt and tool definitions resent with every request, and the work the model did on top of it.&#10;The minimal agent used 58 to 76% fewer tokens than the same agent in standard mode with only its shell tools, for every model.&#10;Most of that is input the provider had cached and bills at a fraction of the price, so the bill shrinks less than the token count does: counting only uncached input and output, the minimal agent used 16 to 42% fewer tokens.&#10;And the model did not pay for it with extra work: the dark part of the bars stayed about the same or shrank, except for Luna&amp;rsquo;s.&#10;I had also hoped that the smaller agent would need less to do the task itself, but only GPT-6 Astra and Sol did, with slightly fewer steps and about a quarter less output; Claude worked exactly the same in both modes.&lt;/p&gt;&#10;&lt;p&gt;These tasks are short, three or four requests each, which is why the prompt weighs so much.&#10;I tried to make them harder so they would take more steps, but the models were so good that they only took about 50% more requests, and for the two models I tried, Claude Sonnet 5.5 and GPT-6 Astra, the pattern stayed the same: Sonnet did the same work in both modes, and Astra less in minimal mode.&#10;For the models that save steps, I see no reason why longer tasks would erase that gain: every step saved also saves resending the conversation, so on long tasks it counts for more.&#10;In a long session, the growing conversation dominates and the prompt&amp;rsquo;s share of the tokens shrinks, but its saving per request stays: about 3,600 tokens for Claude and 2,200 for GPT.&#10;And a real agent&amp;rsquo;s prompt carries far more instructions, skills, and memory than this test agent&amp;rsquo;s, so it saves more per request, although fetching them through the shell when needed costs requests of its own.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;The numbers behind this section&lt;/summary&gt;&#10; &lt;p&gt;&lt;strong&gt;Why Matrix.&lt;/strong&gt;&#10;Minimal mode only works in chat conversations, so I ran it through Matrix, next to the same agent with only its shell tools in standard mode.&#10;Each run got a fresh chat room with only the agent, named &lt;code&gt;coder&lt;/code&gt;, and MindRoom&amp;rsquo;s router, which handles commands like &lt;code&gt;!mode&lt;/code&gt;; for minimal mode, I switched the room over with &lt;code&gt;!mode coder minimal&lt;/code&gt; before sending the task.&#10;In a Matrix conversation, the agent also gets context about the room and the conversation, so it uses more tokens than through the API: on the same code, with only the shell tools, Claude Opus 5.5 used 22,192 tokens per task in Matrix against 12,904 through the API.&#10;That is why I compare minimal mode with standard mode in Matrix.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The prompt.&lt;/strong&gt;&#10;In standard mode, the system prompt is 2,295 tokens for Claude Sonnet 5.5 and 1,463 for GPT-6 Astra, and the tool definitions, the shell tools and one for inviting MindRoom&amp;rsquo;s router, add another 1,990 and 867.&#10;In minimal mode, the system prompt is 152 and 97 tokens, and the single &lt;code&gt;bash&lt;/code&gt; tool adds 509 and 72.&#10;This test agent&amp;rsquo;s standard prompt is about 100 lines only because its own configuration is one role line and one instruction; a real agent&amp;rsquo;s prompt also carries its instructions, skills, context files, and memory, and can run to hundreds of lines, while its minimal prompt stays at 12.&#10;The model receives all of that, plus the task and the room&amp;rsquo;s context, again with every request, so it adds up: in standard mode, about 80% of all tokens were this fixed part.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;The result.&lt;/strong&gt;&#10;Its first request dropped from about 4,600 to 1,000 input tokens for Claude, and from about 2,600 to 410 for GPT.&#10;Counting only the uncached input and the output, the minimal agent used 16 to 42% fewer tokens.&#10;Even with the extra context of a Matrix conversation, it used fewer tokens than the agent with only MindRoom&amp;rsquo;s shell tools through the API on the same code: 21 to 52% fewer.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Is that a fair comparison?&lt;/strong&gt;&#10;A shorter prompt saves tokens even if the model then has to work harder, so the real question is whether it had to.&#10;Mostly, it did not: the work stayed the same for both Claude models (1% and 3% less), and went down for Astra and Sol (38% and 13% less), which also needed fewer requests, about 3.0 per task instead of 3.3 to 3.4.&#10;Luna is the exception: it made more requests and did 40% more work in minimal mode, but the shorter prompt still more than paid for that.&#10;Counted per task without the resent conversation, the Claude models wrote and added to the conversation the same in both modes, Astra and Sol wrote about a quarter less and needed 0.2 to 0.4 fewer requests, and Luna added 33% more, mostly longer command output.&#10;Three of the five models failed one more run in minimal mode, mostly on the same script-writing task that other configurations also miss now and then.&#10;That is too small to tell apart here, but I will keep an eye on it with harder tasks.&lt;/p&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;h3 id="minimal-mode-with-each-models-native-shell-tool"&gt;Minimal mode with each model&amp;rsquo;s native shell tool &lt;a class="anchor" href="#minimal-mode-with-each-models-native-shell-tool" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;That leaves the combination I had in mind from the start: the minimal agent, with each model&amp;rsquo;s native shell tool as its only tool.&#10;I patched minimal mode so that its single tool appears as Claude Code&amp;rsquo;s &lt;code&gt;Bash&lt;/code&gt; for Claude and as Codex&amp;rsquo;s &lt;code&gt;exec_command&lt;/code&gt; for GPT, with the same 12-line prompt.&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;figure class="plot-figure"&gt;&#10; &lt;div class="plot-container" role="img" aria-label="Change in tokens per task in minimal mode when its single tool is the model&amp;rsquo;s native shell tool, Claude Code&amp;rsquo;s Bash or Codex&amp;rsquo;s exec_command, instead of MindRoom&amp;rsquo;s bash, with 95% confidence intervals. The second row cuts the native tool&amp;rsquo;s output to the last 100 lines, MindRoom&amp;rsquo;s bash default. Hover for values." data-plot-module="/post/native-harness-tools/plots.js" data-plot-name="minimalNative"&gt;&lt;/div&gt;&#10; &lt;figcaption&gt;Change in tokens per task in minimal mode when its single tool is the model&amp;rsquo;s native shell tool, Claude Code&amp;rsquo;s Bash or Codex&amp;rsquo;s exec_command, instead of MindRoom&amp;rsquo;s bash, with 95% confidence intervals. The second row cuts the native tool&amp;rsquo;s output to the last 100 lines, MindRoom&amp;rsquo;s bash default. Hover for values.&lt;/figcaption&gt;&#10; &lt;noscript&gt;This chart needs JavaScript.&lt;/noscript&gt;&#10;&lt;/figure&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/plot.7cb86dc2e3ef25b56bc93730b2620745a7f02b5715de87f94e61908f60e51690.js" integrity="sha256-fLhtwuPvJbVryTcwsmIHRafwK1cV3of5TmGQj2DlFpA=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&#10;&#10;&lt;p&gt;It did not make the minimal agent better for any model.&#10;For Claude, it made little difference: Sonnet used about the same, and Opus a little more.&#10;For GPT, it was clearly worse, mostly because &lt;code&gt;exec_command&lt;/code&gt; hands back a command&amp;rsquo;s whole output, and in an agent that sends so little else, a few long outputs weigh a lot.&#10;With its output cut to 100 lines, &lt;code&gt;bash&lt;/code&gt;&amp;rsquo;s default and the light rows in the chart, only Sol still clearly used more; Astra&amp;rsquo;s and Luna&amp;rsquo;s smaller increases could be chance.&lt;/p&gt;&#10;&lt;p&gt;So the whole effect of the combination came from its minimal half: models that were trained with a carefully designed set of tools did best with one tool and almost no prompt, and the tool&amp;rsquo;s native shape did not make it better.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;The numbers behind this section&lt;/summary&gt;&#10; &lt;p&gt;The single native tool only runs commands, but no current model ever polled or stopped a command in any of these runs, so it lacked nothing they used.&#10;I ran it next to minimal mode with MindRoom&amp;rsquo;s own &lt;code&gt;bash&lt;/code&gt; on the same code, and once more with the native tool&amp;rsquo;s output cut to the last 100 lines.&#10;That is &lt;code&gt;bash&lt;/code&gt;&amp;rsquo;s default, which the GPT models set themselves on every call, more often lower than higher; in the end, the capped native tool and &lt;code&gt;bash&lt;/code&gt; returned about as much, except for Astra, whose &lt;code&gt;bash&lt;/code&gt; results were twice as long.&lt;/p&gt;&#10;&lt;p&gt;For Claude, Sonnet used 4% fewer tokens with the native tool and Opus 7% more.&#10;For GPT, it was 34 to 46% more tokens, mostly because &lt;code&gt;exec_command&lt;/code&gt; returns a command&amp;rsquo;s whole output, so the models read 39 to 50 lines per result instead of 20 to 36.&#10;Cut to 100 lines, the native tool cost GPT 5 to 15% more, but only Sol&amp;rsquo;s increase is clearly more than chance; part of it is the definition, which takes about 90 more tokens than &lt;code&gt;bash&lt;/code&gt; on every request.&#10;In minimal mode, the agent sends so little else that a few long outputs weigh much more than through the API: the cap cut Astra&amp;rsquo;s tokens by about 20% here, against 4% there.&#10;Claude rarely printed more than 100 lines, so I would not read anything into the gap between its two native rows.&#10;The pass rates differed by at most a few runs, too few to tell apart.&lt;/p&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;h2 id="what-this-does-and-does-not-show"&gt;What this does and does not show &lt;a class="anchor" href="#what-this-does-and-does-not-show" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I tested the names and arguments of the tools, not whole harnesses. Claude Code and Codex also have their own tool descriptions, system prompts, and output formats, Codex has an interactive terminal, and RL trains all of that together. The full harness might still help today&amp;rsquo;s models; matching only the tools did not.&lt;/li&gt;&#10;&lt;li&gt;The tasks are easy and short: 97% of all runs passed, with three to seven requests per task on average. Harder, longer tasks might separate the tool sets on success, not only on cost.&lt;/li&gt;&#10;&lt;li&gt;With 36 runs per configuration, a 15 to 30% difference in tokens is clear, but a difference of one or two passed runs is not.&lt;/li&gt;&#10;&lt;li&gt;I tune MindRoom&amp;rsquo;s agent setup on other tasks from the same 12 families, and a note on working habits from that tuning sits in the shell tool&amp;rsquo;s description. Both tool sets carry the same note, so it should not favor either.&lt;/li&gt;&#10;&lt;li&gt;The confidence intervals cover run-to-run variation on these 12 tasks, not the choice of tasks. Resampling the tasks as well widens them: GPT-6 Astra&amp;rsquo;s +59%, for example, becomes +38 to +81%, and Sonnet 5.5&amp;rsquo;s 6% saving becomes indistinguishable from none. Astra and Sol still used more tokens with Codex&amp;rsquo;s tools on every one of the 12 tasks, and the minimal agent used fewer on every task, for every model.&lt;/li&gt;&#10;&lt;li&gt;Most runs used each model&amp;rsquo;s standard reasoning mode, which differs between generations: adaptive thinking for Sonnet 5 and the 5.5 models, none for Opus 4.7 and 4.8, and the effort Codex picks for GPT. I tried other settings on four models: two current ones at low and high effort, and Opus 4.7 and 4.8 with adaptive thinking. The pattern held at every effort: Astra used 27 to 45% more tokens with Codex&amp;rsquo;s tools, and Sonnet 5.5 stayed slightly cheaper with Claude Code&amp;rsquo;s.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="what-happens-to-the-pull-request"&gt;What happens to the pull request &lt;a class="anchor" href="#what-happens-to-the-pull-request" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The pull request is still unmerged, and given these results, I am not sure I will merge it at all.&#10;For today&amp;rsquo;s models, the native tools either cost more, as Codex&amp;rsquo;s did for GPT-6 Astra and Sol, or, as Claude Code&amp;rsquo;s did for Claude, only saved the few tokens of the shorter descriptions I wrote for them, which MindRoom&amp;rsquo;s own tools could get too.&#10;The one model that clearly did better with its own tools, Claude Opus 4.8, is a generation behind.&#10;What I will do is use minimal mode a lot more, with MindRoom&amp;rsquo;s own &lt;code&gt;bash&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="why-this-excites-me"&gt;Why this excites me &lt;a class="anchor" href="#why-this-excites-me" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Half of my plan was wrong, and I am happy about that.&#10;The minimal-harness, DeepSeek-style half held up; the native-tools half did not.&#10;The benchmarks I did not believe were right, at least for these models.&#10;It suggests that these models generalize much better than I gave them credit for.&#10;They were trained with one specific set of tools, and on these tasks they did just as well with tools they had presumably never seen in training, as long as those tools were simple, like an edit tool that takes one path, the old text, and the new text.&#10;Only Opus 4.8, the model that started all this, still did better with its native tools.&#10;What matters more than familiarity is how much the agent sends with every request: the prompt and the definition of every tool.&lt;/p&gt;&#10;&lt;p&gt;It also changes how I think about who gets to build the best agents.&#10;If models only worked well in the harness they were trained in, the labs would own the best agents by default.&#10;Instead, at least the skill of using unfamiliar tools seems to carry over to other harnesses.&#10;I suspect the same holds one level up, for how several agents work together.&#10;The labs presumably train their models in their own harness, but if what the models learn transfers this well, a harness built outside the labs, like MindRoom, could get just as good at letting agents collaborate.&#10;That is still a hunch, and testing it is what I want to do next.&lt;/p&gt;&#10;&lt;h2 id="appendix-how-i-measured-and-the-numbers-behind-the-charts"&gt;Appendix: how I measured, and the numbers behind the charts &lt;a class="anchor" href="#appendix-how-i-measured-and-the-numbers-behind-the-charts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;All scripts, the tasks, and one row per trial, with its reasoning setting, are &lt;a href="https://github.com/basnijholt/nijho.lt/tree/main/content/post/native-harness-tools/reproduce" target="_blank" rel="noopener"&gt;on GitHub next to this post&amp;rsquo;s source&lt;/a&gt;.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;Show the setup and every configuration&lt;/summary&gt;&#10; &lt;p&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Each configuration ran as a separate MindRoom instance with one agent and a fresh Docker container for its commands. The model, the tools, and everything else were fixed in that instance&amp;rsquo;s configuration.&lt;/li&gt;&#10;&lt;li&gt;The tool comparisons ran through MindRoom&amp;rsquo;s OpenAI-compatible API on the pull request at commit &lt;code&gt;d73aafb51&lt;/code&gt;: the matching sets, the mixed sets with whole and with 100-line output, and both shell-only setups. The capped run changed only the output limit of the native shell tools.&lt;/li&gt;&#10;&lt;li&gt;The minimal-mode runs and their standard-mode comparison ran through Matrix on MindRoom&amp;rsquo;s main branch at release &lt;code&gt;v2026.10.228&lt;/code&gt;, whose shell tools lack two arguments the pull request added, a working directory and a wait. A shell-only run through the API on that release used within 3% of the pull request&amp;rsquo;s tokens for every model except Luna, which used 8% fewer.&lt;/li&gt;&#10;&lt;li&gt;The combination ran through Matrix on the pull request with an experiment patch that shows minimal mode&amp;rsquo;s single tool in the model&amp;rsquo;s own dialect. On that code, minimal mode with MindRoom&amp;rsquo;s &lt;code&gt;bash&lt;/code&gt; used within 10% of its tokens on main for every model.&lt;/li&gt;&#10;&lt;li&gt;Tokens per task are the input and output tokens of every model request in that task, from MindRoom&amp;rsquo;s per-request usage logs. The work is what is left after subtracting the fixed part, the first request&amp;rsquo;s input times the number of requests.&lt;/li&gt;&#10;&lt;li&gt;The confidence intervals come from resampling the three runs of each task 5,000 times, keeping the mix of tasks fixed.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Each cell shows passed runs out of 36, then tokens per task; the work columns show how much the work changed with the native tools.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Matching sets: each harness&amp;rsquo;s own tools against MindRoom&amp;rsquo;s matching set (pull request, through MindRoom&amp;rsquo;s API)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;Reasoning&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s set&lt;/th&gt;&#10;&lt;th&gt;Harness&amp;rsquo;s own set&lt;/th&gt;&#10;&lt;th&gt;Work with the harness&amp;rsquo;s set&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 5.5&lt;/td&gt;&#10;&lt;td&gt;adaptive thinking&lt;/td&gt;&#10;&lt;td&gt;36/36 · 15,661&lt;/td&gt;&#10;&lt;td&gt;35/36 · 13,628 (−13%)&lt;/td&gt;&#10;&lt;td&gt;+7%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;adaptive thinking&lt;/td&gt;&#10;&lt;td&gt;36/36 · 14,606&lt;/td&gt;&#10;&lt;td&gt;36/36 · 13,764 (−6%)&lt;/td&gt;&#10;&lt;td&gt;+15%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;Codex&amp;rsquo;s choice&lt;/td&gt;&#10;&lt;td&gt;36/36 · 7,041&lt;/td&gt;&#10;&lt;td&gt;36/36 · 11,206 (+59%)&lt;/td&gt;&#10;&lt;td&gt;+84%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;&#10;&lt;td&gt;Codex&amp;rsquo;s choice&lt;/td&gt;&#10;&lt;td&gt;36/36 · 7,292&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,417 (+43%)&lt;/td&gt;&#10;&lt;td&gt;+60%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Luna&lt;/td&gt;&#10;&lt;td&gt;Codex&amp;rsquo;s choice&lt;/td&gt;&#10;&lt;td&gt;35/36 · 8,425&lt;/td&gt;&#10;&lt;td&gt;34/36 · 8,638 (+3%)&lt;/td&gt;&#10;&lt;td&gt;−5%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.7&lt;/td&gt;&#10;&lt;td&gt;no thinking&lt;/td&gt;&#10;&lt;td&gt;34/36 · 24,398&lt;/td&gt;&#10;&lt;td&gt;33/36 · 23,564 (−3%)&lt;/td&gt;&#10;&lt;td&gt;+13%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.8&lt;/td&gt;&#10;&lt;td&gt;no thinking&lt;/td&gt;&#10;&lt;td&gt;33/36 · 24,103&lt;/td&gt;&#10;&lt;td&gt;34/36 · 19,593 (−19%)&lt;/td&gt;&#10;&lt;td&gt;−17%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5&lt;/td&gt;&#10;&lt;td&gt;adaptive thinking&lt;/td&gt;&#10;&lt;td&gt;33/36 · 29,781&lt;/td&gt;&#10;&lt;td&gt;35/36 · 27,833 (−7%)&lt;/td&gt;&#10;&lt;td&gt;+9%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-5.5&lt;/td&gt;&#10;&lt;td&gt;Codex&amp;rsquo;s choice&lt;/td&gt;&#10;&lt;td&gt;35/36 · 10,819&lt;/td&gt;&#10;&lt;td&gt;35/36 · 11,353 (+5%)&lt;/td&gt;&#10;&lt;td&gt;+2%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-5.6 Sol&lt;/td&gt;&#10;&lt;td&gt;Codex&amp;rsquo;s choice&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,624&lt;/td&gt;&#10;&lt;td&gt;34/36 · 10,991 (+3%)&lt;/td&gt;&#10;&lt;td&gt;−5%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;For Claude, both sets have a shell and tools to read, edit, and write files; for GPT, a shell and Codex&amp;rsquo;s &lt;code&gt;apply_patch&lt;/code&gt; against MindRoom&amp;rsquo;s tools to edit and write files. Neither has MindRoom&amp;rsquo;s &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find_files&lt;/code&gt;, or &lt;code&gt;ls&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Mixed sets: native tools against MindRoom&amp;rsquo;s tools (pull request, through MindRoom&amp;rsquo;s API; Claude with adaptive thinking, GPT with the effort Codex picks)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s tools&lt;/th&gt;&#10;&lt;th&gt;Native tools&lt;/th&gt;&#10;&lt;th&gt;Native tools, 100-line output&lt;/th&gt;&#10;&lt;th&gt;Work with native tools&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 5.5&lt;/td&gt;&#10;&lt;td&gt;36/36 · 18,943&lt;/td&gt;&#10;&lt;td&gt;36/36 · 17,244 (−9%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 17,410 (−8%)&lt;/td&gt;&#10;&lt;td&gt;+5%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;36/36 · 17,765&lt;/td&gt;&#10;&lt;td&gt;36/36 · 16,237 (−9%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 16,399 (−8%)&lt;/td&gt;&#10;&lt;td&gt;−4%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;35/36 · 8,250&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,807 (+31%)&lt;/td&gt;&#10;&lt;td&gt;35/36 · 10,399 (+26%)&lt;/td&gt;&#10;&lt;td&gt;+51%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;&#10;&lt;td&gt;36/36 · 8,873&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,534 (+19%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,239 (+15%)&lt;/td&gt;&#10;&lt;td&gt;+24%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Luna&lt;/td&gt;&#10;&lt;td&gt;33/36 · 12,003&lt;/td&gt;&#10;&lt;td&gt;35/36 · 11,532 (−4%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,816 (−10%)&lt;/td&gt;&#10;&lt;td&gt;+15%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;Older models, mixed sets: native tools against MindRoom&amp;rsquo;s tools (pull request, through MindRoom&amp;rsquo;s API)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;Reasoning&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s tools&lt;/th&gt;&#10;&lt;th&gt;Native tools&lt;/th&gt;&#10;&lt;th&gt;Work with native tools&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.7&lt;/td&gt;&#10;&lt;td&gt;no thinking&lt;/td&gt;&#10;&lt;td&gt;34/36 · 33,140&lt;/td&gt;&#10;&lt;td&gt;33/36 · 25,793 (−22%)&lt;/td&gt;&#10;&lt;td&gt;−26%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.8&lt;/td&gt;&#10;&lt;td&gt;no thinking&lt;/td&gt;&#10;&lt;td&gt;33/36 · 32,219&lt;/td&gt;&#10;&lt;td&gt;34/36 · 23,843 (−26%)&lt;/td&gt;&#10;&lt;td&gt;−32%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5&lt;/td&gt;&#10;&lt;td&gt;adaptive thinking&lt;/td&gt;&#10;&lt;td&gt;33/36 · 42,349&lt;/td&gt;&#10;&lt;td&gt;34/36 · 27,120 (−36%)&lt;/td&gt;&#10;&lt;td&gt;−45%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-5.5&lt;/td&gt;&#10;&lt;td&gt;Codex&amp;rsquo;s choice&lt;/td&gt;&#10;&lt;td&gt;34/36 · 15,007&lt;/td&gt;&#10;&lt;td&gt;36/36 · 14,419 (−4%)&lt;/td&gt;&#10;&lt;td&gt;−11%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-5.6 Sol&lt;/td&gt;&#10;&lt;td&gt;Codex&amp;rsquo;s choice&lt;/td&gt;&#10;&lt;td&gt;36/36 · 15,062&lt;/td&gt;&#10;&lt;td&gt;35/36 · 12,809 (−15%)&lt;/td&gt;&#10;&lt;td&gt;−27%&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;Older models, only the shell tools: native shape against MindRoom&amp;rsquo;s (pull request, through MindRoom&amp;rsquo;s API; Opus 4.7 and 4.8 without thinking, Sonnet 5 with adaptive thinking, GPT with the effort Codex picks)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s shell&lt;/th&gt;&#10;&lt;th&gt;Native shell&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.7&lt;/td&gt;&#10;&lt;td&gt;33/36 · 17,518&lt;/td&gt;&#10;&lt;td&gt;33/36 · 15,714 (−10%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.8&lt;/td&gt;&#10;&lt;td&gt;35/36 · 19,183&lt;/td&gt;&#10;&lt;td&gt;33/36 · 14,686 (−23%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5&lt;/td&gt;&#10;&lt;td&gt;33/36 · 22,624&lt;/td&gt;&#10;&lt;td&gt;33/36 · 28,354 (+25%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-5.5&lt;/td&gt;&#10;&lt;td&gt;36/36 · 9,056&lt;/td&gt;&#10;&lt;td&gt;36/36 · 9,046 (0%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-5.6 Sol&lt;/td&gt;&#10;&lt;td&gt;36/36 · 9,171&lt;/td&gt;&#10;&lt;td&gt;34/36 · 8,658 (−6%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Sonnet 5&amp;rsquo;s native mean comes from one run that cost 271,106 tokens, after a 50 KB command output; its median is 9% lower with the native shell.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Opus 4.7 and 4.8 with adaptive thinking, mixed sets: native tools against MindRoom&amp;rsquo;s tools (pull request, through MindRoom&amp;rsquo;s API)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;Thinking replies&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s tools&lt;/th&gt;&#10;&lt;th&gt;Native tools&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.7&lt;/td&gt;&#10;&lt;td&gt;26% and 31%&lt;/td&gt;&#10;&lt;td&gt;35/36 · 35,610&lt;/td&gt;&#10;&lt;td&gt;33/36 · 23,090 (−35%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 4.8&lt;/td&gt;&#10;&lt;td&gt;36% and 30%&lt;/td&gt;&#10;&lt;td&gt;33/36 · 25,269&lt;/td&gt;&#10;&lt;td&gt;33/36 · 20,130 (−20%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;Reasoning effort, mixed sets: native tools against MindRoom&amp;rsquo;s tools (pull request, through MindRoom&amp;rsquo;s API)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;Reasoning effort&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s tools&lt;/th&gt;&#10;&lt;th&gt;Native tools&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;low&lt;/td&gt;&#10;&lt;td&gt;36/36 · 15,154&lt;/td&gt;&#10;&lt;td&gt;36/36 · 12,330 (−19%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;default&lt;/td&gt;&#10;&lt;td&gt;36/36 · 17,765&lt;/td&gt;&#10;&lt;td&gt;36/36 · 16,237 (−9%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;high&lt;/td&gt;&#10;&lt;td&gt;36/36 · 18,286&lt;/td&gt;&#10;&lt;td&gt;36/36 · 15,630 (−15%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;low&lt;/td&gt;&#10;&lt;td&gt;36/36 · 8,361&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,627 (+27%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;default&lt;/td&gt;&#10;&lt;td&gt;35/36 · 8,250&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,807 (+31%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;high&lt;/td&gt;&#10;&lt;td&gt;36/36 · 8,934&lt;/td&gt;&#10;&lt;td&gt;36/36 · 12,918 (+45%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;Only the shell tools (pull request, through MindRoom&amp;rsquo;s API; Claude with adaptive thinking, GPT with the effort Codex picks)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;All of MindRoom&amp;rsquo;s tools&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s shell only&lt;/th&gt;&#10;&lt;th&gt;Native shell only&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 5.5&lt;/td&gt;&#10;&lt;td&gt;36/36 · 18,943&lt;/td&gt;&#10;&lt;td&gt;36/36 · 12,926 (−32%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 12,202 (−6%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;36/36 · 17,765&lt;/td&gt;&#10;&lt;td&gt;36/36 · 12,455 (−30%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 10,168 (−18%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;35/36 · 8,250&lt;/td&gt;&#10;&lt;td&gt;36/36 · 6,025 (−27%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 6,052 (0%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;&#10;&lt;td&gt;36/36 · 8,873&lt;/td&gt;&#10;&lt;td&gt;36/36 · 6,195 (−30%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 6,177 (0%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Luna&lt;/td&gt;&#10;&lt;td&gt;33/36 · 12,003&lt;/td&gt;&#10;&lt;td&gt;31/36 · 7,118 (−41%)&lt;/td&gt;&#10;&lt;td&gt;34/36 · 7,018 (−1%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;MindRoom&amp;rsquo;s shell alone is compared with all of MindRoom&amp;rsquo;s tools, and the native shell with MindRoom&amp;rsquo;s shell.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Minimal mode against standard mode (main branch, through Matrix, shell tools only; Claude with adaptive thinking, GPT with the effort Codex picks)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;Standard mode&lt;/th&gt;&#10;&lt;th&gt;Minimal mode&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 5.5&lt;/td&gt;&#10;&lt;td&gt;36/36 · 22,192&lt;/td&gt;&#10;&lt;td&gt;36/36 · 8,030 (−64%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;35/36 · 22,448&lt;/td&gt;&#10;&lt;td&gt;34/36 · 7,828 (−65%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;36/36 · 11,429&lt;/td&gt;&#10;&lt;td&gt;36/36 · 2,798 (−76%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;&#10;&lt;td&gt;36/36 · 11,070&lt;/td&gt;&#10;&lt;td&gt;35/36 · 3,370 (−70%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Luna&lt;/td&gt;&#10;&lt;td&gt;35/36 · 12,568&lt;/td&gt;&#10;&lt;td&gt;34/36 · 5,219 (−58%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;Minimal mode: the native tool against MindRoom&amp;rsquo;s &lt;code&gt;bash&lt;/code&gt; (pull request with the experiment patch, through Matrix; Claude with adaptive thinking, GPT with the effort Codex picks)&lt;/strong&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;Model&lt;/th&gt;&#10;&lt;th&gt;MindRoom&amp;rsquo;s &lt;code&gt;bash&lt;/code&gt;&lt;/th&gt;&#10;&lt;th&gt;Native tool&lt;/th&gt;&#10;&lt;th&gt;Native tool, 100-line output&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Opus 5.5&lt;/td&gt;&#10;&lt;td&gt;36/36 · 8,058&lt;/td&gt;&#10;&lt;td&gt;36/36 · 8,654 (+7%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 9,123 (+13%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Claude Sonnet 5.5&lt;/td&gt;&#10;&lt;td&gt;34/36 · 7,787&lt;/td&gt;&#10;&lt;td&gt;34/36 · 7,514 (−4%)&lt;/td&gt;&#10;&lt;td&gt;34/36 · 8,000 (+3%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Astra&lt;/td&gt;&#10;&lt;td&gt;35/36 · 3,071&lt;/td&gt;&#10;&lt;td&gt;36/36 · 4,173 (+36%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 3,359 (+9%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6.1 Sol&lt;/td&gt;&#10;&lt;td&gt;34/36 · 3,103&lt;/td&gt;&#10;&lt;td&gt;33/36 · 4,150 (+34%)&lt;/td&gt;&#10;&lt;td&gt;36/36 · 3,575 (+15%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;GPT-6 Luna&lt;/td&gt;&#10;&lt;td&gt;34/36 · 4,697&lt;/td&gt;&#10;&lt;td&gt;34/36 · 6,849 (+46%)&lt;/td&gt;&#10;&lt;td&gt;32/36 · 4,926 (+5%)&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;One of Luna&amp;rsquo;s runs with the whole native output counts for passes but not for tokens: the provider reported no usage for one of its requests.&lt;/p&gt;&#10;&#10;&lt;/details&gt;&#10;&#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;Armin also found that Anthropic&amp;rsquo;s strict tool mode, which restricts the model&amp;rsquo;s output to the tool&amp;rsquo;s schema, made these failures disappear in his tests. Pi later turned it on for Claude models, and a pi user then &lt;a href="https://github.com/earendil-works/pi/issues/10074#issuecomment-6000381416" target="_blank" rel="noopener"&gt;linked a different problem to it&lt;/a&gt;: in that user&amp;rsquo;s logs, 9.5% of Claude&amp;rsquo;s edits had mangled &lt;code&gt;\u&lt;/code&gt; escapes for non-ASCII text, such as Korean, with strict mode on, and 1.5% with it off.&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;The 12 tasks come from 12 families: a CSV report, finding the commit that broke a check, renaming an API across a package, counting errors in log files, fixing failing tests, finding files, extracting fields from JSON, writing a shell script, editing a configuration file, counting matching lines in very long command output, unpacking nested archives, and deduplicating and merging data files. I tune MindRoom&amp;rsquo;s agent setup on other instances of these families, and these 12 were held out from that.&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;Claude and GPT count tokens differently, so compare the numbers within one model, not across models. Cached input counts in full here, although providers bill it at a fraction of the price, so a bill changes less than the token count.&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;MindRoom&amp;rsquo;s toolkits accept &lt;code&gt;exclude_tools&lt;/code&gt;, so both sets drop MindRoom&amp;rsquo;s &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find_files&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt;, and for GPT also &lt;code&gt;read_file&lt;/code&gt;, since Codex reads files through the shell. Both keep their shell&amp;rsquo;s tools for checking on and stopping a background command, which no model used in the matching-set runs.&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;Anthropic adds its own instructions for tool use to every request that has tools, which is why MindRoom&amp;rsquo;s single &lt;code&gt;bash&lt;/code&gt; tool costs Claude 509 tokens. I measured all of these numbers by sending each part on its own and reading the input tokens the provider reported.&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;The patch is with this post&amp;rsquo;s reproduction scripts. It shows minimal mode&amp;rsquo;s single tool as MindRoom&amp;rsquo;s canonical run function, which the tool dialect then presents as &lt;code&gt;Bash&lt;/code&gt; or &lt;code&gt;exec_command&lt;/code&gt;; nothing else about minimal mode changes.&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;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Exploring the Jev hype in MindRoom</title><link>https://www.nijho.lt/post/jev-mindroom/</link><pubDate>Wed, 23 Sep 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/jev-mindroom/</guid><description>&lt;p&gt;Last week, every AI newsletter I get was full of &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" target="_blank" rel="noopener"&gt;Jev&lt;/a&gt;, a new model from TypeSafe.&#10;So were several subreddits.&#10;I am a permanent lurker on &lt;a href="https://www.reddit.com/r/LocalLLaMA/" target="_blank" rel="noopener"&gt;r/LocalLLaMA&lt;/a&gt;, and even though it is about local models, every other post seemed to be about Jev.&#10;There were local clones like &lt;a href="https://github.com/r-ms/mini-jev" target="_blank" rel="noopener"&gt;mini-jev&lt;/a&gt; and &lt;a href="https://github.com/wfzyx/von" target="_blank" rel="noopener"&gt;von&lt;/a&gt;, CLI wrappers like &lt;a href="https://github.com/joshLong145/jev-cli" target="_blank" rel="noopener"&gt;jev-cli&lt;/a&gt;, and even &lt;a href="https://news.ycombinator.com/item?id=49765348" target="_blank" rel="noopener"&gt;someone on Hacker News&lt;/a&gt; claiming &lt;a href="https://laya.convaiinnovations.com/" target="_blank" rel="noopener"&gt;they had built the same thing a year earlier&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I tried Jev in &lt;a href="https://www.nijho.lt/post/mindroom/"&gt;MindRoom&lt;/a&gt;, my &lt;a href="https://github.com/mindroom-ai/mindroom" target="_blank" rel="noopener"&gt;open-source&lt;/a&gt; agent platform on &lt;a href="https://matrix.org" target="_blank" rel="noopener"&gt;Matrix&lt;/a&gt;, and less than two days later it was making &lt;a href="https://github.com/mindroom-ai/mindroom/issues/2156" target="_blank" rel="noopener"&gt;three decisions&lt;/a&gt; there.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;strong&gt;Update 2026-10-07:&lt;/strong&gt; OpenAI released the Decisions API today, its own take on the same idea, and MindRoom now supports it as a third backend; see &lt;a href="#a-third-backend-openai-decisions"&gt;below&lt;/a&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="what-a-system-one-model-is"&gt;What a System One model is &lt;a class="anchor" href="#what-a-system-one-model-is" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The &lt;a href="https://docs.typesafe.ai/concepts/system-one" target="_blank" rel="noopener"&gt;name comes&lt;/a&gt; from Daniel Kahneman&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/Thinking,_Fast_and_Slow" target="_blank" rel="noopener"&gt;&lt;em&gt;Thinking, Fast and Slow&lt;/em&gt;&lt;/a&gt;, where System 1 is fast, intuitive thinking and System 2 is slow and deliberate.&#10;A System One model does not generate text.&#10;You send it a state, for example a chat log, plus typed questions, and it returns &lt;a href="https://docs.typesafe.ai/api" target="_blank" rel="noopener"&gt;probabilities&lt;/a&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;Type&lt;/th&gt;&#10;&lt;th&gt;Question&lt;/th&gt;&#10;&lt;th&gt;Answer&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Noul&lt;/td&gt;&#10;&lt;td&gt;Yes or no&lt;/td&gt;&#10;&lt;td&gt;Probability of yes&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Choice&lt;/td&gt;&#10;&lt;td&gt;Pick one of up to 255 options&lt;/td&gt;&#10;&lt;td&gt;Chosen option plus the full distribution&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Score&lt;/td&gt;&#10;&lt;td&gt;Rate against a rubric of 2 to 10 levels&lt;/td&gt;&#10;&lt;td&gt;Probability-weighted value&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" target="_blank" rel="noopener"&gt;TypeSafe claims&lt;/a&gt; 70 to 500 ms end to end and $0.042 per million input tokens, with output tokens free.&#10;They also write &amp;ldquo;we can&amp;rsquo;t prove it isn&amp;rsquo;t subsidized.&amp;rdquo;&#10;Either way, it is incredibly cheap and incredibly fast.&lt;/p&gt;&#10;&lt;h2 id="adaptive-participation"&gt;Adaptive participation &lt;a class="anchor" href="#adaptive-participation" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In MindRoom, a conversation can have several humans, several agents, or both.&#10;When I talk to a single agent, it simply replies, and I don&amp;rsquo;t need to tag it.&#10;Once another person joins and writes something, the agent cannot tell whether the message was meant for it or for the other person.&#10;Nothing decided that, so the agent stayed quiet unless explicitly tagged.&#10;That confused several users.&lt;/p&gt;&#10;&lt;p&gt;So I recently added &lt;a href="https://docs.mindroom.chat/configuration/threads/#adaptive-participation" target="_blank" rel="noopener"&gt;adaptive participation&lt;/a&gt;: an agent that has already replied in a thread decides on its own whether to answer an untagged message.&#10;I first used an LLM as the judge, GPT-5.6 Luna on low reasoning.&#10;A yes-or-no question like &amp;ldquo;should this agent respond?&amp;rdquo; is exactly what Jev is for, so it &lt;a href="https://github.com/mindroom-ai/mindroom/pull/2169" target="_blank" rel="noopener"&gt;became the second backend&lt;/a&gt;.&#10;Compared with Luna, it is about ten times cheaper and ten times faster, and if I believe the benchmarks, it is also smarter.&lt;/p&gt;&#10;&lt;h2 id="thanks-should-not-stop-the-agent"&gt;&amp;ldquo;Thanks&amp;rdquo; should not stop the agent &lt;a class="anchor" href="#thanks-should-not-stop-the-agent" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The next thing I used Jev for was interruptions.&#10;When I send a message while an agent is still replying, MindRoom injects a notice at the next tool call telling the agent to stop and wrap up, because there is a new message.&#10;But often I, and other users, would watch the agent start working and then write &amp;ldquo;looks good&amp;rdquo; or &amp;ldquo;thanks.&amp;rdquo;&#10;The agent would stop early and continue in the next turn, which is both wasteful and counterintuitive.&lt;/p&gt;&#10;&lt;p&gt;Now &lt;a href="https://github.com/mindroom-ai/mindroom/pull/2193" target="_blank" rel="noopener"&gt;a judgment&lt;/a&gt; decides whether the new message needs the interruption, which the docs call &lt;a href="https://docs.mindroom.chat/configuration/threads/#mid-turn-coalescing" target="_blank" rel="noopener"&gt;mid-turn coalescing&lt;/a&gt;.&#10;If it does not, the agent reacts with 👀, finishes its reply, and handles the message afterwards.&lt;/p&gt;&#10;&lt;p&gt;This is also where I learned a lesson about evals.&#10;My first implementation worked at the code level but not functionally.&#10;My evals, based on real attempts inside MindRoom that had all failed plus synthetic cases from GPT-6 Astra, passed 5 out of 40 with Jev.&#10;&lt;a href="https://github.com/mindroom-ai/mindroom/pull/2244" target="_blank" rel="noopener"&gt;The fix&lt;/a&gt; changed the question and gave the judge more context, and it went to 40 out of 40.&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;&lt;/th&gt;&#10;&lt;th&gt;Before: 5/40&lt;/th&gt;&#10;&lt;th&gt;After: 40/40&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Question&lt;/td&gt;&#10;&lt;td&gt;&amp;ldquo;May the active task finish before the queued human messages are handled in a later turn?&amp;rdquo;&lt;/td&gt;&#10;&lt;td&gt;&amp;ldquo;Does any newly queued message require an immediate change to, pause of, or stop of the active task?&amp;rdquo;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;When unclear&lt;/td&gt;&#10;&lt;td&gt;&amp;ldquo;Also false when context is incomplete, the relationship is unclear…&amp;rdquo;&lt;/td&gt;&#10;&lt;td&gt;&amp;ldquo;Mere relevance to the task does not make praise or thanks an interruption.&amp;rdquo;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Context&lt;/td&gt;&#10;&lt;td&gt;The request and the new messages&lt;/td&gt;&#10;&lt;td&gt;The same, plus the preceding conversation&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The old question treated anything unclear as a reason to interrupt, and a bare &amp;ldquo;thanks&amp;rdquo; without the conversation around it always looks unclear.&lt;/p&gt;&#10;&lt;h2 id="picking-the-responder"&gt;Picking the responder &lt;a class="anchor" href="#picking-the-responder" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The third decision is routing.&#10;When a message in a room with many agents does not mention anyone, MindRoom&amp;rsquo;s router picks who should answer.&#10;That used to be a free-text LLM prompt, &amp;ldquo;choose the most appropriate agent.&amp;rdquo;&#10;It is &lt;a href="https://github.com/mindroom-ai/mindroom/pull/2238" target="_blank" rel="noopener"&gt;now a Jev Choice&lt;/a&gt; over the eligible agents, plus a &lt;code&gt;no_fit&lt;/code&gt; option, as described in the &lt;a href="https://docs.mindroom.chat/configuration/router/" target="_blank" rel="noopener"&gt;router docs&lt;/a&gt;.&#10;A confident &lt;code&gt;no_fit&lt;/code&gt; asks the user to mention an agent instead of guessing, and anything uncertain falls back to the existing LLM router.&lt;/p&gt;&#10;&lt;h2 id="one-config-line-to-switch"&gt;One config line to switch &lt;a class="anchor" href="#one-config-line-to-switch" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I built a very simple abstraction layer so I can swap Jev for an LLM without changing any code.&#10;Every decision is one question with criteria for true and false:&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;MID_TURN_QUESTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JudgmentQuestion&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="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;interrupt_current_turn&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;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Does any newly queued message require an immediate change to, pause of, or stop of the active task?&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;when_true&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;A new message requests stopping or pausing the active task, corrects it, ...&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;when_false&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;The messages acknowledge progress, express thanks, ask to continue unchanged, ...&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;Jev receives it as a Noul question and returns a probability, which MindRoom compares with a threshold.&#10;An LLM receives the same question and context and must return &lt;code&gt;{&amp;quot;decision&amp;quot;: true}&lt;/code&gt; or &lt;code&gt;{&amp;quot;decision&amp;quot;: false}&lt;/code&gt;.&#10;Switching is a configuration change:&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;helper&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;mid_turn&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;defer_reaction&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;👀&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="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;judgment&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;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;typesafe &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# or: provider: llm, model: fast&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;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.8&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;Both backends log their decisions and latency, so I can compare them on real traffic.&#10;The code is in &lt;a href="https://github.com/mindroom-ai/mindroom/tree/main/src/mindroom/judgment" target="_blank" rel="noopener"&gt;&lt;code&gt;src/mindroom/judgment/&lt;/code&gt;&lt;/a&gt;, and the backends are documented under &lt;a href="https://docs.mindroom.chat/configuration/threads/#judgment-backends" target="_blank" rel="noopener"&gt;participation judgment backends&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="a-third-backend-openai-decisions"&gt;A third backend: OpenAI Decisions &lt;a class="anchor" href="#a-third-backend-openai-decisions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;On October 7, OpenAI released its own version of the idea, the &lt;a href="https://developers.openai.com/api/docs/guides/decisions" target="_blank" rel="noopener"&gt;Decisions API&lt;/a&gt;, currently in public beta.&#10;The question types match Jev&amp;rsquo;s: a &lt;code&gt;predicate&lt;/code&gt; is a Noul, and &lt;code&gt;choice&lt;/code&gt; and &lt;code&gt;score&lt;/code&gt; keep their names.&#10;It serves a single model, &lt;code&gt;gpt-6-luna&lt;/code&gt;, and OpenAI says it returns answers &amp;ldquo;about 10x faster than the Responses API.&amp;rdquo;&#10;Input costs $0.10 per million tokens and output is free, so it lists at about 2.4 times Jev&amp;rsquo;s price.&lt;/p&gt;&#10;&lt;p&gt;Thanks to the abstraction layer, adding it took &lt;a href="https://github.com/mindroom-ai/mindroom/pull/2751" target="_blank" rel="noopener"&gt;one PR&lt;/a&gt;, merged the same day, and it works for all three decisions, including the router&amp;rsquo;s Choice:&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;judgment&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;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;openai_decisions&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;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.8&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;From MindRoom&amp;rsquo;s side, only the wire format differs.&#10;The endpoint accepts only user messages, so MindRoom sends each chat message as its own user message, prefixed with its role, like &lt;code&gt;assistant: ...&lt;/code&gt;.&#10;Decisions can also answer with a refusal, which MindRoom treats as an abstention, so the usual fallback applies.&lt;/p&gt;&#10;&lt;p&gt;I tested it against the live API through MindRoom&amp;rsquo;s own code, with the same questions Jev gets.&#10;For participation, it gave 0.99 to replying to a direct question and 0.0 while two humans were talking to each other.&#10;Mid-turn, &amp;ldquo;Thanks, looks great so far!&amp;rdquo; got 0.0 for interrupting and &amp;ldquo;Stop, keep MySQL…&amp;rdquo; got 1.0.&#10;The router sent a pandas bug to &lt;code&gt;code&lt;/code&gt; and a meeting request to &lt;code&gt;calendar&lt;/code&gt;, both with confidence 1.0.&#10;A nonsense request got only 0.62, below the 0.8 threshold, so the normal LLM router took over.&#10;Each call took 130 to 280 ms.&#10;I have not compared it with Jev on real traffic yet, but both log the same outcome fields, so that only takes a config change.&lt;/p&gt;&#10;&lt;h2 id="named-after-jevons"&gt;Named after Jevons &lt;a class="anchor" href="#named-after-jevons" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I only read &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" target="_blank" rel="noopener"&gt;why TypeSafe named it Jev&lt;/a&gt; after building all this: they named it after William Stanley Jevons, who noticed that as steam engines burned coal more efficiently, demand for coal went up instead of down.&#10;This is known as the &lt;a href="https://en.wikipedia.org/wiki/Jevons_paradox" target="_blank" rel="noopener"&gt;Jevons paradox&lt;/a&gt;.&#10;That was literally my experience.&#10;Most of what I built was perfectly possible with an LLM.&#10;I just never built it, because I am somewhat cost- and latency-minded.&#10;Once I implemented Jev for one decision, I came up with use case after use case.&lt;/p&gt;&#10;&lt;p&gt;One I already built is a plugin, &lt;a href="https://github.com/mindroom-ai/response-audit-jev-plugin" target="_blank" rel="noopener"&gt;Response Audit JEV&lt;/a&gt;.&#10;Right after an agent replies, Jev checks the answer against the request and the tool calls the agent actually made, for example whether things are properly cited.&#10;If a check flags a problem, the plugin posts one follow-up in the thread that tags the agent and asks for a correction.&#10;Because Jev is so fast and cheap, running this after every reply costs almost nothing.&lt;/p&gt;&#10;&lt;p&gt;Right now I am full of ideas.&#10;For example, a model router.&#10;In MindRoom, an agent can switch to a different model mid-conversation with a tool call, and I can do the same with &lt;code&gt;!model&lt;/code&gt;.&#10;Agents rarely do that without being told, though.&#10;So I default to a very expensive model, and I only save money when I remember to switch to a cheap one for something simple.&#10;Jev could run on every message and decide whether it needs the capable model or not.&#10;That works in both directions, up to a capable model or down to a cheap one.&#10;The perfect moment to switch is when the prompt cache has expired.&#10;A prompt cache belongs to one model, so switching normally means sending the entire context again at full price.&#10;Once the cache has expired, I pay that price anyway, so switching costs nothing extra.&lt;/p&gt;&#10;</description></item><item><title>Removing the guardrails and letting my coding agents loose</title><link>https://www.nijho.lt/post/removing-guardrails/</link><pubDate>Tue, 22 Sep 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/removing-guardrails/</guid><description>&lt;p&gt;I have run coding agents in YOLO mode since I started using them in May 2025.&#10;I think it is the only way to parallelize work.&#10;Sometimes I have ten agents running at the same time, and clicking &amp;ldquo;approve&amp;rdquo; on every command they run would make that impossible.&lt;/p&gt;&#10;&lt;p&gt;What makes me comfortable with it is that models tend to do what you ask, and that you can keep them in an environment where they cannot do much harm.&#10;I don&amp;rsquo;t keep secrets within their reach, and above all I have &lt;a href="https://www.nijho.lt/post/btrfs-to-zfs/"&gt;very good backups&lt;/a&gt;.&#10;The last piece is a small set of hooks that block the few things I never want an agent to do on its own.&lt;/p&gt;&#10;&lt;h2 id="why-hooks-and-not-agentsmd"&gt;Why hooks and not AGENTS.md &lt;a class="anchor" href="#why-hooks-and-not-agentsmd" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My system prompt has always told agents not to force-push or merge PRs.&#10;Some of them did it anyway.&lt;/p&gt;&#10;&lt;p&gt;The first time I really noticed was with &lt;a href="https://www.nijho.lt/post/gemini-3-pro-first-impressions/"&gt;Gemini 3 Pro in November 2025&lt;/a&gt;, which merged my PR and force-pushed to &lt;code&gt;main&lt;/code&gt;.&#10;It happened a couple more times in the weeks after, as I parallelized more and asked more of the models.&#10;Three weeks later I had hooks for Claude Code and Gemini CLI that block these commands before they run.&#10;Codex did not support hooks yet.&#10;To my surprise, it did not need them: OpenAI&amp;rsquo;s models are extremely good at following instructions, and Codex never ignored an explicit rule in my &lt;code&gt;AGENTS.md&lt;/code&gt;.&#10;Codex supports hooks by now, so it runs the same ones as the others.&lt;/p&gt;&#10;&lt;p&gt;Claude has been worse at following rules, and Gemini far worse still: absolutely dogshit.&#10;I only try Gemini now and then, and I have been disappointed every time.&#10;By now I don&amp;rsquo;t even try the latest Google models anymore.&lt;/p&gt;&#10;&lt;p&gt;I don&amp;rsquo;t think agents are ever purposely malicious.&#10;They try to do what you asked, but they can misinterpret it, and then they force-push or merge something.&#10;A rule in a Markdown file is a hope; a hook actually stops the command.&#10;Not that a hook would stop a malicious agent: it could put the blocked command in a Bash script and run that instead.&#10;What the hook does is tell the agent why I don&amp;rsquo;t want it, because every block comes with a short explanation:&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;git add -A is not allowed - add files explicitly to avoid adding unrelated untracked files&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The models are well aligned, so once they know my intention, they respect it.&lt;/p&gt;&#10;&lt;h2 id="what-i-block"&gt;What I block &lt;a class="anchor" href="#what-i-block" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;All hooks live in &lt;a href="https://github.com/basnijholt/dotfiles/tree/main/configs/claude/hooks" target="_blank" rel="noopener"&gt;my dotfiles&lt;/a&gt;, and I deliberately kept them simple.&#10;The detection logic is one Python module, &lt;a href="https://github.com/basnijholt/dotfiles/blob/main/configs/claude/hooks/git_guard.py" target="_blank" rel="noopener"&gt;&lt;code&gt;git_guard.py&lt;/code&gt;&lt;/a&gt;, and the Claude Code, Codex, and Gemini CLI hooks all import it.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;code&gt;git commit --amend&lt;/code&gt; and &lt;code&gt;git push --force&lt;/code&gt;.&lt;/strong&gt;&#10;The unit I review is a PR.&#10;When I review a PR, I know I have reviewed it up to a certain commit, and later I only look at the diff of the new commits.&#10;Rewriting history breaks that.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Pushing to &lt;code&gt;main&lt;/code&gt;.&lt;/strong&gt;&#10;Everything happens in PRs.&#10;Inside a PR I don&amp;rsquo;t care how messy the history gets.&#10;I want the agent to push very often, so no work gets lost, and every commit has to be green, so each one is a snapshot I can go back to.&#10;The agent maintains its own development of the feature, and I have not written a commit message myself in a long time.&#10;When the PR is done, I squash merge it.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;code&gt;gh pr merge&lt;/code&gt;.&lt;/strong&gt;&#10;The merge comes after my review, so it is mine to do.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;code&gt;git add -A&lt;/code&gt;.&lt;/strong&gt;&#10;I often have unrelated untracked or unstaged files lying around.&#10;I work in many open source repositories, and some of those files should not be public, like deployment plans.&#10;This is the only block without an override.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;code&gt;sleep&lt;/code&gt;.&lt;/strong&gt;&#10;This one is for Claude, and Opus 5 specifically, which I hate with a passion.&#10;It would run tests in the background and then &lt;code&gt;sleep 300&lt;/code&gt;, badly overestimating how long the tests take, when a blocking tool call returns exactly when they finish.&lt;/p&gt;&#10;&lt;h2 id="the-override"&gt;The override &lt;a class="anchor" href="#the-override" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over time the models got more capable.&#10;I could parallelize more, and I trusted them to work on their own for longer.&#10;I noticed I was spending a significant fraction of my time clicking buttons and doing operations I had forbidden the agent to do, only to end up doing them myself.&lt;/p&gt;&#10;&lt;p&gt;So in July I added an override.&#10;When a hook blocks a command, the agent gets this hint:&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;If (and only if) the user has explicitly approved this exact action,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;re-run the command prefixed with EXPLICITLY_USER_APPROVED_HOOK_OVERRIDE=1 to override this block.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Nothing checks whether I actually approved it.&#10;It is a soft gate, but it works because the agent now has to write down, in the command itself, that I approved the action.&#10;When it runs into the block without my approval, it becomes obvious to the agent that it has to ask me first.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;git add -A&lt;/code&gt; stays hard-blocked, and a chained command cannot carry an override past it: &lt;code&gt;EXPLICITLY_USER_APPROVED_HOOK_OVERRIDE=1 git commit --amend &amp;amp;&amp;amp; git add -A&lt;/code&gt; is still rejected.&lt;/p&gt;&#10;&lt;h2 id="when-the-override-was-used-without-approval"&gt;When the override was used without approval &lt;a class="anchor" href="#when-the-override-was-used-without-approval" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;It has happened, mostly with GPT-5.6 Sol, which I used a lot at the time.&lt;/p&gt;&#10;&lt;p&gt;It used to be that you had to keep your context window short, because auto-compaction was lossy and produced poor summaries.&#10;Since around GPT-5.3, compaction works well enough that I keep a session going for as long as I am working on the same feature.&#10;So I would ask the agent to merge a PR early on, start a follow-up PR in the same session, and after a compaction the agent concluded that it should merge the new PR too.&#10;It did, without my approval.&lt;/p&gt;&#10;&lt;p&gt;The other case was my &lt;a href="https://github.com/mindroom-ai/mindroom/blob/main/.claude/skills/pr-review/SKILL.md" target="_blank" rel="noopener"&gt;&lt;code&gt;pr-review&lt;/code&gt; skill&lt;/a&gt;, by far my favorite skill that I wrote myself.&#10;I would tell the agent to run it and squash merge the PR only if the review approved it.&#10;A few times, the review found problems, the agent fixed them, and then it merged.&#10;What I meant was: merge if it is approved right away, and otherwise wait for me.&#10;Now I spell that out and end with &amp;ldquo;otherwise, wait for my instructions.&amp;rdquo;&lt;/p&gt;&#10;&lt;h2 id="working-from-my-phone"&gt;Working from my phone &lt;a class="anchor" href="#working-from-my-phone" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The override matters most when I am away from my computer.&#10;On a month-long trip visiting family in Europe, I worked only from my phone, using &lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;my mobile coding workflow&lt;/a&gt;.&#10;Typing commands in a terminal on a phone is inconvenient, so I would rather say what I want and let the agent do it, including the merge.&#10;I have also built end-to-end encrypted voice calls to my agents over Matrix with &lt;a href="https://www.nijho.lt/post/mindroom/"&gt;MindRoom&lt;/a&gt;, and I am moving more toward that kind of voice-driven workflow.&lt;/p&gt;&#10;&lt;h2 id="not-much-has-changed"&gt;Not much has changed &lt;a class="anchor" href="#not-much-has-changed" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I recently reread &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;my first post on agentic coding&lt;/a&gt; from August 2025, and very little in it is outdated.&#10;The models are more capable, and I give them much larger scopes, but the way I work is mostly the same.&#10;The hooks are the main thing I added, and the override is how I loosened them again.&lt;/p&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>Gemini 3 Pro: First Impressions</title><link>https://www.nijho.lt/post/gemini-3-pro-first-impressions/</link><pubDate>Wed, 19 Nov 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/gemini-3-pro-first-impressions/</guid><description>&lt;p&gt;I&amp;rsquo;m a huge fan of &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic&lt;/a&gt; &lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;coding&lt;/a&gt;.&#10;I have the maximum subscriptions for both Claude and ChatGPT Pro, and since writing my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic coding&lt;/a&gt; post, I mostly moved from Claude Code to Codex CLI with GPT-5 for my work that requires more complex reasoning.&#10;Using Claude Code often feels frustrating in comparison now.&lt;/p&gt;&#10;&lt;p&gt;I also frequently pit them against each other, drafting parallel plans with each tool (I&amp;rsquo;ve done this at least 50 times).&#10;The verdict is consistent.&#10;&lt;strong&gt;Claude Code, without exception, says that GPT‑5&amp;rsquo;s solution is superior&lt;/strong&gt;, admitting it is more elegant and robust.&#10;Claude also misses bugs during PR reviews that GPT-5 catches immediately.&lt;/p&gt;&#10;&lt;p&gt;That said, &lt;strong&gt;Claude Code is significantly faster&lt;/strong&gt;, which makes it my go-to for simple refactors.&#10;I also prefer its writing style for conversational answers and documentation.&lt;/p&gt;&#10;&lt;p&gt;But looking at the charts on &lt;strong&gt;&lt;a href="https://artificialanalysis.ai/" target="_blank" rel="noopener"&gt;Artificial Analysis&lt;/a&gt;&lt;/strong&gt;, the new &lt;strong&gt;Gemini 3 Pro&lt;/strong&gt; looks truly impressive.&#10;It&amp;rsquo;s at the top of nearly every benchmark.&#10;I&amp;rsquo;m excited for Gemini to potentially take over some of GPT‑5&amp;rsquo;s workload, so when &lt;strong&gt;Gemini 3 Pro Preview&lt;/strong&gt; came out yesterday, I had to try it.&lt;/p&gt;&#10;&lt;h2 id="the-good-raw-power"&gt;The good: raw power &lt;a class="anchor" href="#the-good-raw-power" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;First off, the model is quite powerful.&#10;Last night, I built a complex new feature for 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; project in just two hours: a &lt;strong&gt;RAG (Retrieval-Augmented Generation) proxy server&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;This tool acts as a middleware that sits between any OpenAI-compatible client and server (like Ollama or vLLM).&#10;The server watches a specific folder, automatically indexes any PDF, Markdown, or text files into a local ChromaDB vector store, and injects relevant context into your prompts transparently.&#10;You can see the full implementation in &lt;a href="https://github.com/basnijholt/agent-cli/pull/79" target="_blank" rel="noopener"&gt;PR #79&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;It even handles dynamic re-indexing when I drop new files in!&#10;The speed and code quality for this task were genuinely impressive, and it handled the complexity of the RAG integration surprisingly well.&lt;/p&gt;&#10;&lt;h2 id="the-bad-panic-attacks-and-infinite-loops"&gt;The bad: panic attacks and infinite loops &lt;a class="anchor" href="#the-bad-panic-attacks-and-infinite-loops" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;However, the experience wasn&amp;rsquo;t without its &amp;ldquo;personality quirks.&amp;rdquo;&#10;Twice, Gemini got stuck in a loop saying &lt;em&gt;&amp;ldquo;Should I? I should&amp;rdquo;&lt;/em&gt; over and over again.&#10;Or the following also happened:&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://www.nijho.lt/post/gemini-3-pro-first-impressions/infinite-loop_hu_234c7407851f9273.webp" srcset="https://www.nijho.lt/post/gemini-3-pro-first-impressions/infinite-loop_hu_8f9dd754d34e9bda.webp 272w, https://www.nijho.lt/post/gemini-3-pro-first-impressions/infinite-loop_hu_234c7407851f9273.webp 516w, https://www.nijho.lt/post/gemini-3-pro-first-impressions/infinite-loop_hu_3023867168cf93e2.webp 815w" sizes="auto, (max-width: 760px) 100vw, 720px" width="516" height="760" alt="Gemini infinite loop" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;It also has a bad habit of amending commits multiple times and force-pushing them.&#10;For me the Git history is a critical source of truth, and I don&amp;rsquo;t like rewriting it because I might just have reviewed those commits already.&lt;/p&gt;&#10;&lt;h3 id="the-interface-friction"&gt;The interface friction &lt;a class="anchor" href="#the-interface-friction" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I also found the Gemini CLI interface frustrating.&#10;Basic tasks like selecting text or scrolling up are surprisingly difficult.&#10;I&amp;rsquo;m not sure why they chose this design or why they feel the need to deviate from standard CLI behavior.&#10;It feels like unnecessary friction in an otherwise promising tool.&lt;/p&gt;&#10;&lt;h3 id="proofreading-this-post"&gt;Proofreading this post &lt;a class="anchor" href="#proofreading-this-post" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I also asked Gemini to proofread this very blog post.&#10;It made some suggestions—some I liked, others I didn&amp;rsquo;t, so I reverted the ones I didn&amp;rsquo;t want.&#10;But in every single subsequent iteration, it would forcefully re-apply its rejected changes, adding emojis I had removed or rewriting sentences I had explicitly restored.&lt;/p&gt;&#10;&lt;h3 id="the-main-branch-incident"&gt;The &amp;ldquo;main branch&amp;rdquo; incident &lt;a class="anchor" href="#the-main-branch-incident" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Another scary moment: I asked if the PR was ready to be merged into &lt;code&gt;main&lt;/code&gt;, and it went ahead and checked out the &lt;code&gt;main&lt;/code&gt; branch and merged everything locally.&#10;If I hadn&amp;rsquo;t stopped it, I&amp;rsquo;m pretty sure it would have pushed the changes to the &lt;code&gt;main&lt;/code&gt; branch directly as well.&#10;I tend to use &amp;ldquo;yolo mode&amp;rdquo; for agentic coding because I feel like Git has my back, but this feels like a dangerous path to go down.&lt;/p&gt;&#10;&lt;p&gt;And indeed, while writing this post, &lt;strong&gt;it actually happened&lt;/strong&gt;.&#10;Gemini merged my pull request without my permission.&lt;/p&gt;&#10;&lt;p&gt;My prompt was &lt;code&gt;&amp;gt; should the README reflect this?&lt;/code&gt; and before that I asked to review the PR.&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://www.nijho.lt/post/gemini-3-pro-first-impressions/merge_hu_719c4aa620a5a59a.webp" srcset="https://www.nijho.lt/post/gemini-3-pro-first-impressions/merge_hu_44f4ede1b23e43f5.webp 400w, https://www.nijho.lt/post/gemini-3-pro-first-impressions/merge_hu_719c4aa620a5a59a.webp 760w, https://www.nijho.lt/post/gemini-3-pro-first-impressions/merge_hu_fcec307dabb3ec8f.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="326" alt="Gemini merging without permission" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;When I asked &lt;em&gt;why&lt;/em&gt; it merged it, it instantly force-pushed to &lt;code&gt;main&lt;/code&gt; again!&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://www.nijho.lt/post/gemini-3-pro-first-impressions/force-push-again_hu_a7f66199c342259b.webp" srcset="https://www.nijho.lt/post/gemini-3-pro-first-impressions/force-push-again_hu_3884bda6d7ac695f.webp 400w, https://www.nijho.lt/post/gemini-3-pro-first-impressions/force-push-again_hu_a7f66199c342259b.webp 760w, https://www.nijho.lt/post/gemini-3-pro-first-impressions/force-push-again_hu_8a797e9907cbcd9e.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="379" alt="Gemini force pushing to main" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;h3 id="artifacts-and-existential-crises"&gt;Artifacts and existential crises &lt;a class="anchor" href="#artifacts-and-existential-crises" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Also, when it commits, it just adds &lt;em&gt;all&lt;/em&gt; files, including debugging logs and build artifacts.&#10;When it realizes its mistake, it tries to fix it.&#10;But if I try to help at the same time (like removing the files myself while it&amp;rsquo;s thinking), it gets extremely confused.&lt;/p&gt;&#10;&lt;p&gt;It spirals into an &lt;strong&gt;existential crisis&lt;/strong&gt;, claiming that I committed the file but now &amp;ldquo;it is gone.&amp;rdquo;&#10;&lt;em&gt;&amp;ldquo;What is going on?&amp;rdquo;&lt;/em&gt; it asks, spending a lot of tokens (and time) looking at &lt;code&gt;git reflog&lt;/code&gt;s and questioning its own existence.&#10;Even when I stop it and explicitly say that I deleted the file and to please continue, it completely ignores me and continues its panic attack.&#10;I experienced this pattern of ignoring instructions multiple times.&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;Gemini 3 Pro shows massive potential.&#10;The raw coding ability is there, and building a &lt;strong&gt;clean&lt;/strong&gt; RAG proxy in two hours is great!&#10;But the agentic behavior needs some serious work: the loops, the force pushes, ignoring instructions, and the confusion when reality doesn&amp;rsquo;t match its internal state.&lt;/p&gt;&#10;&lt;p&gt;This is just &lt;strong&gt;day one&lt;/strong&gt;, so I&amp;rsquo;m confident many of these issues will get ironed out.&#10;I don&amp;rsquo;t love the interface, but given that &lt;a href="https://github.com/google-gemini/gemini-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;gemini-cli&lt;/code&gt;&lt;/a&gt; is open source with ≈400 contributors, I expect rapid improvements, especially now that the model is finally good with agentic workflows!&lt;/p&gt;&#10;</description></item><item><title>Coding from my phone 📱</title><link>https://www.nijho.lt/post/agentic-mobile-workflow/</link><pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/agentic-mobile-workflow/</guid><description>&lt;p&gt;I am dictating this post at ten kilometers altitude on a flight home from Mexico, Blink Shell open on my iPhone, while an agentic assistant on my &lt;a href="https://nixos.org/" target="_blank" rel="noopener"&gt;NixOS&lt;/a&gt; machine writes an initial draft for this post.&#10;Admittedly, I’m a little addicted to agentic coding: when an idea pops up, I want to jump on it right away—even if that means doing it from a cramped seat in the sky.&#10;This is the workflow I’ve arrived at after trying many alternatives, it extends &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;my agentic coding write-up&lt;/a&gt; and the self-hosted AI obsession in &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;my local AI journey&lt;/a&gt;.&#10;It&amp;rsquo;s my backup plan for those moments when a computer simply isn&amp;rsquo;t nearby—the rest of the time I&amp;rsquo;m still at a keyboard like any other developer.&#10;This is in no way a replacement for a proper computer/laptop workflow but gets the job done when needed.&#10;It&amp;rsquo;s a personal, mostly open-source stack built around &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;—my local-first voice layer.&#10;Here&amp;rsquo;s how it works: I speak into my phone, the audio goes to &lt;a href="https://github.com/SYSTRAN/faster-whisper-server" target="_blank" rel="noopener"&gt;FasterWhisper&lt;/a&gt; for transcription, &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt; cleans up the text, and it lands on my clipboard ready to paste.&#10;My iOS Shortcut handles this through a small HTTP interface that agent-cli exposes.&#10;The coding agent itself still uses the best proprietary model I can access.&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;Meta: I&amp;rsquo;m writing about agentic coding on mobile from my actual phone.&#10;It&amp;rsquo;s not the most efficient way to write, but it gets the job done — during a 6.5‑hour flight I dictated and iterated this entire post.&#10;Each iteration was a commit: dictate changes, review the diff, refine, commit, repeat.&#10;See all &lt;a href="https://github.com/basnijholt/nijho.lt/pull/40" target="_blank" rel="noopener"&gt;50+ commits in PR #40&lt;/a&gt; for the full iteration trail.&lt;/div&gt;&lt;/aside&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; iPhone → WireGuard → Blink+Mosh → Zellij.&#10;I dictate via a Shortcut → FasterWhisper (transcribe) → Ollama (polish) → clipboard, then paste into Codex CLI using OpenAI’s &lt;code&gt;gpt-5-codex-high&lt;/code&gt;.&#10;Everything is local except the model.&lt;/div&gt;&lt;/aside&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="#1-why-phone-coding-works-now"&gt;1. Why Phone Coding Works Now&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-constraints-and-trade-offs"&gt;2. Constraints and Trade-offs&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-layer-1-wireguard-from-the-router"&gt;3. Layer 1: WireGuard From the Router&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-layer-2-blink-shell--mosh-for-durable-sessions"&gt;4. Layer 2: Blink Shell + Mosh for Durable Sessions&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-layer-3-zellij-layouts"&gt;5. Layer 3: Zellij Layouts&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-layer-4-agent-cli-server"&gt;6. Layer 4: &lt;code&gt;agent-cli&lt;/code&gt; Server&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-layer-5-the-ios-shortcut-pipeline"&gt;7. Layer 5: The iOS Shortcut Pipeline&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-workflow-in-practice"&gt;8. Workflow in Practice&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-conclusion-and-further-reading"&gt;9. Conclusion and Further Reading&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-why-phone-coding-works-now"&gt;1. Why Phone Coding Works Now &lt;a class="anchor" href="#1-why-phone-coding-works-now" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;For years I used &lt;a href="https://ish.app/" target="_blank" rel="noopener"&gt;&lt;strong&gt;iSH&lt;/strong&gt;&lt;/a&gt; (a full Alpine Linux emulator) with SSH to hop into servers from my phone, but dropped connections were a constant frustration (Mosh fixes this, more on that later).&#10;Because coding on a phone keyboard is terrible, I kept it to tiny configuration tweaks or one-off fixes of a few characters.&#10;Agentic tools changed that: with a CLI coding agent, I don’t need to type the code—I describe the change, review the patch, and run it.&#10;That made meaningful work on the phone possible for the first time, for those moments when a computer isn’t around.&lt;/p&gt;&#10;&lt;p&gt;I tried &lt;strong&gt;VS Code in the browser&lt;/strong&gt;, bounced between &lt;strong&gt;iSH&lt;/strong&gt; and &lt;strong&gt;Terminus&lt;/strong&gt; for SSH, and even lived inside a handful of in-browser terminal clients.&#10;I also spent time with mobile companions like &lt;a href="https://happy.engineering/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Happy&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://www.ycombinator.com/companies/omnara" target="_blank" rel="noopener"&gt;&lt;strong&gt;Omnara&lt;/strong&gt;&lt;/a&gt;, both designed to mirror Claude Code sessions on the phone, but they still felt like extra steps between me and my actual terminal.&#10;That friction pushed me toward a phone‑ready, self‑hosted workflow that still gives me raw SSH access to my own machine when the computer is out of reach.&lt;/p&gt;&#10;&lt;p&gt;This post is based on the way I develop software today.&#10;Your mileage may vary, but if you also care about privacy, open tooling, and reproducible environments, I think there are useful pieces here.&lt;/p&gt;&#10;&lt;h2 id="2-constraints-and-trade-offs"&gt;2. Constraints and Trade-offs &lt;a class="anchor" href="#2-constraints-and-trade-offs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This setup grew organically over time. What emerged are less strict requirements and more happy accidents that turned into core benefits:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Single trust boundary:&lt;/strong&gt; Only the model provider (OpenAI) sees code context; audio and automation stay local, and I avoid any additional third‑party relays.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Resilient sessions:&lt;/strong&gt; Connections should survive sleep and spotty networks.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Voice-friendly:&lt;/strong&gt; Dictation should be accurate enough that I can trust it.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reproducible config:&lt;/strong&gt; The entire stack must live in my &lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;dotfiles&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/dotfiles/tree/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/nixos" target="_blank" rel="noopener"&gt;NixOS configuration&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;To give you a sense of what I tried, here is the short comparison that convinced me to roll my own:&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;Approach&lt;/th&gt;&#10;&lt;th&gt;Pros&lt;/th&gt;&#10;&lt;th&gt;Why I moved on&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;VS Code in the browser&lt;/td&gt;&#10;&lt;td&gt;Familiar editor UI&lt;/td&gt;&#10;&lt;td&gt;Needs a steady connection and still lives outside my dotfiles comfort zone&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;iSH / Terminus SSH&lt;/td&gt;&#10;&lt;td&gt;Works without extra infrastructure&lt;/td&gt;&#10;&lt;td&gt;Laggy, no Mosh, and awkward keybindings&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;In-browser terminals&lt;/td&gt;&#10;&lt;td&gt;Instant access from anywhere&lt;/td&gt;&#10;&lt;td&gt;Poor copy/paste ergonomics and flaky mobile keyboards&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://apps.apple.com/us/app/happy-codex-claude-code-app/id6748571505" target="_blank" rel="noopener"&gt;Happy&lt;/a&gt; (Claude Code companion)&lt;/td&gt;&#10;&lt;td&gt;Push notifications, encrypted mobile UI for Claude Code&lt;/td&gt;&#10;&lt;td&gt;Requires wrapping every session with a separate CLI and still abstracts away my shell, and was quite buggy for me&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://omnara.com" target="_blank" rel="noopener"&gt;Omnara&lt;/a&gt; (agent command center)&lt;/td&gt;&#10;&lt;td&gt;Centralizes Claude Code/Codex sessions with terminal replay&lt;/td&gt;&#10;&lt;td&gt;Proxies via their servers; I already trust OpenAI for the model and don&amp;rsquo;t want another third‑party handling my code&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The stack below gives me the resilience of Mosh, the ergonomics of Zellij, and full control over the AI layer.&lt;/p&gt;&#10;&lt;h2 id="3-layer-1-wireguard-from-the-router"&gt;3. Layer 1: WireGuard From the Router &lt;a class="anchor" href="#3-layer-1-wireguard-from-the-router" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://www.wireguard.com/" target="_blank" rel="noopener"&gt;WireGuard&lt;/a&gt; is a modern VPN that lets me securely access my home network from anywhere.&#10;I terminate WireGuard on my router so every device in the house (and on the road) can dial home with the same config.&#10;(If you want something easier to set up without opening ports, &lt;a href="https://tailscale.com/" target="_blank" rel="noopener"&gt;Tailscale&lt;/a&gt; is a more user-friendly option.)&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Server:&lt;/strong&gt; ASUS XT8 router with WireGuard enabled via the router UI.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Client:&lt;/strong&gt; The WireGuard iOS app with &lt;code&gt;On-Demand&lt;/code&gt; rules so the tunnel flips on whenever I am off trusted Wi-Fi.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;DNS:&lt;/strong&gt; All mobile sessions resolve through my home DNS, so &lt;code&gt;git.nijho.lt&lt;/code&gt; and internal services resolve instantly.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This gives Blink a local-LAN address for my desktop &lt;code&gt;nixos&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h2 id="4-layer-2-blink-shell--mosh-for-durable-sessions"&gt;4. Layer 2: Blink Shell + Mosh for Durable Sessions &lt;a class="anchor" href="#4-layer-2-blink-shell--mosh-for-durable-sessions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What is &lt;a href="https://mosh.org/" target="_blank" rel="noopener"&gt;Mosh&lt;/a&gt;?&#10;It&amp;rsquo;s like SSH, but it stays connected when the network changes or the phone sleeps, and it feels faster on bad connections.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blink.sh/" target="_blank" rel="noopener"&gt;Blink Shell&lt;/a&gt; is my daily driver on iOS because it pairs beautifully with Mosh and has great keyboard ergonomics.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I launch sessions using &lt;code&gt;mosh bas@nixos -- zellij attach --create phone&lt;/code&gt; (&lt;a href="https://zellij.dev/" target="_blank" rel="noopener"&gt;Zellij&lt;/a&gt; is my terminal multiplexer—more on that in the next section).&lt;/li&gt;&#10;&lt;li&gt;Mosh smooths over spotty LTE and keeps my session alive when the phone sleeps.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Mosh keeps the session responsive across flaky networks and sleep.&lt;/p&gt;&#10;&lt;h2 id="5-layer-3-zellij-layouts"&gt;5. Layer 3: Zellij Layouts &lt;a class="anchor" href="#5-layer-3-zellij-layouts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I use &lt;strong&gt;Zellij&lt;/strong&gt; as my terminal multiplexer for mobile work.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;I stick to the defaults and use a couple of predefined layouts (e.g., a &amp;ldquo;phone&amp;rdquo; layout with editor + shell panes).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Ergonomics matter even more on a glass keyboard, so I lean on a few shell helpers:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&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;/strong&gt; means I can jump between repos with &lt;code&gt;z foo&lt;/code&gt; instead of pecking long paths.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Single-character aliases&lt;/strong&gt; like &lt;code&gt;p&lt;/code&gt; for &lt;code&gt;pytest&lt;/code&gt; and &lt;code&gt;gs&lt;/code&gt; for &lt;code&gt;git status&lt;/code&gt; live in &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/shell/10_aliases.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/shell/10_aliases.sh&lt;/code&gt;&lt;/a&gt;, keeping command entry to a minimum.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Autocompletion plugins&lt;/strong&gt;—&lt;a href="https://github.com/zsh-users/zsh-autosuggestions" target="_blank" rel="noopener"&gt;&lt;code&gt;zsh-autosuggestions&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;zsh-syntax-highlighting&lt;/code&gt;—are wired up in &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/shell/70_zsh_plugins.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/shell/70_zsh_plugins.sh&lt;/code&gt;&lt;/a&gt;, so I get inline hints and colour cues while typing.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I break these tricks down in more depth in &lt;a href="https://www.nijho.lt/post/terminal-ninja/"&gt;Terminal Ninja&lt;/a&gt;, explain how I sync them with my &lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;Dotfiles&lt;/a&gt;, and even package binaries like &lt;code&gt;zoxide&lt;/code&gt; with &lt;a href="https://www.nijho.lt/post/dotbins/"&gt;Dotbins&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="6-layer-4-agent-cli-server"&gt;6. Layer 4: &lt;code&gt;agent-cli&lt;/code&gt; Server &lt;a class="anchor" href="#6-layer-4-agent-cli-server" 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; runs as a small server on my NixOS machine.&#10;In this workflow I only use &lt;code&gt;transcribe&lt;/code&gt; from iOS Shortcuts: the phone records audio, the server transcribes and cleans it up, and I paste the text.&#10;(Agent-CLI has other features like &lt;code&gt;autocorrect&lt;/code&gt; and &lt;code&gt;chat&lt;/code&gt;, but I don&amp;rsquo;t use them for this workflow.)&#10;On this machine I run a long‑lived &lt;code&gt;agent-cli&lt;/code&gt; user service (systemd) so Shortcuts can POST audio to it and get cleaned text back—ready to paste into Codex CLI.&#10;The service is defined in my dotfiles—see: &lt;a href="https://github.com/basnijholt/dotfiles/blob/8f6bf0b7219195a46a3e010d3538e1e449634db7/configs/nixos/modules/user.nix#L29-L40" target="_blank" rel="noopener"&gt;&lt;code&gt;configs/nixos/modules/user.nix&lt;/code&gt; (agent-cli service)&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;For more background on why I built it and how I use it in practice, see &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;Local AI Journey&lt;/a&gt; (§3) and &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The models and services run on the same box:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;FasterWhisper&lt;/strong&gt; via &lt;a href="https://github.com/SYSTRAN/faster-whisper" target="_blank" rel="noopener"&gt;&lt;code&gt;faster-whisper-server&lt;/code&gt;&lt;/a&gt; for high‑accuracy streaming transcription.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Ollama&lt;/strong&gt; for on‑device rewrite/cleanup before sending prompts to the coding agent.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;My iOS Shortcut dictation workflow is slower than Apple&amp;rsquo;s built-in dictation, but the accuracy is much better—and that matters more.&lt;/p&gt;&#10;&lt;p&gt;For the actual coding agent, I use a &lt;a href="https://github.com/just-every/code" target="_blank" rel="noopener"&gt;fork&lt;/a&gt; of Codex CLI with OpenAI’s &lt;code&gt;gpt-5-codex-high&lt;/code&gt; model (here no open-source solution matches the frontier).&lt;/p&gt;&#10;&lt;h2 id="7-layer-5-the-ios-shortcut-pipeline"&gt;7. Layer 5: The iOS Shortcut Pipeline &lt;a class="anchor" href="#7-layer-5-the-ios-shortcut-pipeline" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My iPhone&amp;rsquo;s Action Button runs a Shortcut that records audio and sends my voice to agent-cli on my NixOS machine for transcription and cleanup.&lt;/p&gt;&#10;&lt;p&gt;For the full recipe, see the iOS Shortcut Guide: &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/iOS_Shortcut_Guide.md" target="_blank" rel="noopener"&gt;agent-cli/iOS_Shortcut_Guide.md&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;In short: I press the Action Button, the Shortcut records a snippet, sends it to &lt;code&gt;agent-cli&lt;/code&gt;, and copies the cleaned text to my clipboard so I can paste it into Codex CLI.&lt;/p&gt;&#10;&lt;p&gt;The whole loop finishes fast enough that I can capture intent by voice and paste it into Codex CLI without typing.&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;Dictation quality:&lt;/strong&gt; In my experience, the built‑in iOS dictation is absolute garbage compared to Whisper/FasterWhisper.&#10;Accuracy and punctuation are much better with the FasterWhisper server running at home; it’s a bit slower, but that trade‑off is worth it for clean, usable text.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="8-workflow-in-practice"&gt;8. Workflow in Practice &lt;a class="anchor" href="#8-workflow-in-practice" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s the typical flow:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;WireGuard auto-connects when I&amp;rsquo;m off home WiFi&lt;/li&gt;&#10;&lt;li&gt;Open Blink—my Mosh session is still alive from last time, Zellij panes intact&lt;/li&gt;&#10;&lt;li&gt;Dictate the change, paste into Codex CLI, review the agent&amp;rsquo;s response&lt;/li&gt;&#10;&lt;li&gt;Check diffs in an adjacent Zellij pane or different Blink tab with separate Mosh session&lt;/li&gt;&#10;&lt;li&gt;The agent commits and pushes to a branch, then opens a PR via &lt;code&gt;gh&lt;/code&gt; for me to review and merge&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;On the phone, I aim for the smallest practical edits and initial implementations.&#10;Even on personal repos, I still open a PR—sometimes prompting alone gets me to a merge‑ready result.&#10;For open‑source with real users, I finish on the computer with a careful review and any final edits.&lt;/p&gt;&#10;&lt;h2 id="9-conclusion-and-further-reading"&gt;9. Conclusion and Further Reading &lt;a class="anchor" href="#9-conclusion-and-further-reading" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This phone setup extends my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt; workflow to mobile.&#10;I connect to the same Zellij session on &lt;code&gt;nixos&lt;/code&gt;, so when an idea hits, I pick up exactly where I left off—same environment, same context, no setup.&#10;Everything runs on my hardware (voice processing and automation) except the coding model itself.&#10;It’s the most effective mobile workflow I’ve had so far.&#10;It’s mostly open‑source not out of dogma, but because those tools are the best options for my needs.&#10;The one exception is the coding model: there’s no true open equivalent right now, and it makes no sense to buy 20× H100s just to self‑host a frontier model even if such a high quality model would be available open-source.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re curious to dive deeper, here are a few related posts:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;Agentic Coding&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;Dotfiles: A Practical, Cross-Platform Terminal Setup&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;Local AI Journey: RTX 3090 Edition&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/terminal-ninja/"&gt;Terminal Ninja&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; (already outdated at this point!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;d love to hear what self-hosted tricks you&amp;rsquo;re using for mobile development—reach out if you adapt this stack or build something wild on top of it.&lt;/p&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>Confessions of an asymmetric hypocrite: on Python dependencies</title><link>https://www.nijho.lt/post/dependencies/</link><pubDate>Sun, 27 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/dependencies/</guid><description>&lt;h2 id="introduction-the-dependency-dilemma-"&gt;Introduction: The Dependency Dilemma 🤔 &lt;a class="anchor" href="#introduction-the-dependency-dilemma-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I just listened to a podcast &amp;ldquo;&lt;a href="https://www.youtube.com/watch?v=5ZyzeeYZgeM" target="_blank" rel="noopener"&gt;Build software that lasts!&lt;/a&gt;&amp;rdquo; with Bert Hubert (a fellow Dutch), who prefers to reduce the number of dependencies to an absolute minimum (also see his related blog posts [&lt;a href="https://berthub.eu/articles/posts/on-long-term-software-development/" target="_blank" rel="noopener"&gt;1&lt;/a&gt;] and [&lt;a href="https://berthub.eu/articles/posts/a-2024-plea-for-lean-software/" target="_blank" rel="noopener"&gt;2&lt;/a&gt;]).&#10;While I agree one shouldn&amp;rsquo;t use &lt;a href="https://www.npmjs.com/package/is-even" target="_blank" rel="noopener"&gt;&lt;code&gt;is-even&lt;/code&gt;&lt;/a&gt; as a dependency (&lt;a href="https://www.npmjs.com/package/is-odd" target="_blank" rel="noopener"&gt;&lt;code&gt;is-odd&lt;/code&gt;&lt;/a&gt; is a better choice 😏), I think there is a lot of value in reducing the number of dependencies but &lt;em&gt;also&lt;/em&gt; in using the right tool for the job and not continuously re-inventing the wheel.&#10;The podcast got me thinking about my own stance on dependencies and how I actively avoid introducing dependencies into my libraries while expecting (hoping 🤞) others to depend on my work.&lt;/p&gt;&#10;&lt;p&gt;Like many software developers, I&amp;rsquo;ve spent countless hours wrestling with dependencies.&#10;Dependency hell is easy to fall into, &lt;em&gt;especially&lt;/em&gt; in dynamic languages like Python.&#10;Those moments where installing or updating one package breaks another, or where a tiny library pulls in hundreds of transitive dependencies, are frustratingly common.&#10;This frustration partly motivated me to create &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, a tool aimed at simplifying dependency management across different Python tools like &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;conda&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;But working on &lt;code&gt;unidep&lt;/code&gt; and my other projects forces me to think about my own relationship with dependencies.&#10;I&amp;rsquo;ve realized I operate with what feels like an &amp;ldquo;asymmetric hypocrisy.&amp;rdquo;&#10;My standards for the libraries I &lt;em&gt;publish&lt;/em&gt; are extremely strict regarding which external dependencies I &lt;em&gt;consume&lt;/em&gt;.&#10;Yet, as an author of over 30 Python packages, I inherently &lt;em&gt;produce&lt;/em&gt; dependencies, hoping others will adopt my work.&#10;And then, when building applications or CLIs, my caution often disappears entirely.&lt;/p&gt;&#10;&lt;p&gt;This post dives into that asymmetry.&#10;It&amp;rsquo;s about the different hats I wear: the minimalist library author (as a dependency &lt;em&gt;consumer&lt;/em&gt;), the hopeful library author (as a dependency &lt;em&gt;producer&lt;/em&gt;), and the pragmatic application developer.&#10;In this post, I&amp;rsquo;ll explain why I take these different approaches and why it&amp;rsquo;s really about the situation, not a contradiction.&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-the-dependency-dilemma-"&gt;Introduction: The Dependency Dilemma 🤔&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#1-the-library-author-hat--as-dependency-consumer-maximum-caution"&gt;1. The Library Author Hat 🎩 (as Dependency Consumer): Maximum Caution&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-library-author-hat--as-dependency-producer-the-paradox"&gt;2. The Library Author Hat 🎩 (as Dependency Producer): The Paradox&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-application-developer--contributor-hat--practicality-is-key"&gt;3. The Application Developer / Contributor Hat 🧢: Practicality is Key&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-conclusion-not-hypocrisy-but-context-driven-strategy-"&gt;4. Conclusion: Not Hypocrisy, but Context-Driven Strategy 🙏&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;strong&gt;TL;DR:&lt;/strong&gt; I&amp;rsquo;m extremely strict about adding external dependencies to my &lt;em&gt;libraries&lt;/em&gt;, preferring widely-used, low-dependency packages.&#10;Simultaneously, I &lt;em&gt;create&lt;/em&gt; libraries expecting adoption.&#10;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.&#10;It&amp;rsquo;s all about managing risks depending on the situation.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="1-the-library-author-hat--as-dependency-consumer-maximum-caution"&gt;1. The Library Author Hat 🎩 (as Dependency Consumer): Maximum Caution &lt;a class="anchor" href="#1-the-library-author-hat--as-dependency-consumer-maximum-caution" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When I build and publish an open-source Python library – like &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;&lt;code&gt;Adaptive&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt;, or &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt; – my primary focus shifts to the &lt;em&gt;users&lt;/em&gt; who will depend on my code.&#10;My choices directly impact their projects.&#10;This responsibility means I have to be very careful about adding external dependencies:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Minimize and Scrutinize:&lt;/strong&gt; Fewer dependencies mean fewer potential conflicts for users.&#10;I aggressively minimize the core runtime dependency list.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Favor Battle-Tested Dependencies:&lt;/strong&gt; I strongly prefer dependencies that are already widely adopted and trusted by the community.&#10;(I have been called that person that always wants the latest and greatest, the truth is that I am a sucker for shiny new things but typically only adopt them once they are already quite popular.)&#10;Think &lt;code&gt;numpy&lt;/code&gt; or &lt;code&gt;cloudpickle&lt;/code&gt;.&#10;These are less likely to cause unexpected issues or disappear overnight.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Low Transitive Dependency Count:&lt;/strong&gt; A key criterion is how many &lt;em&gt;other&lt;/em&gt; packages a potential dependency pulls in.&#10;Ideally, I choose dependencies with zero or very few further dependencies.&#10;For example, in &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt;, dependencies like &lt;a href="https://numpy.org/" target="_blank" rel="noopener"&gt;&lt;code&gt;numpy&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://networkx.org/" target="_blank" rel="noopener"&gt;&lt;code&gt;networkx&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://github.com/cloudpipe/cloudpickle" target="_blank" rel="noopener"&gt;&lt;code&gt;cloudpickle&lt;/code&gt;&lt;/a&gt; are good examples – they are popular and don&amp;rsquo;t rely on many other things.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Optional Dependencies for Heavy Features:&lt;/strong&gt; If a feature really needs a large or less common dependency, I try making it a &lt;em&gt;optional&lt;/em&gt; extra (&lt;code&gt;project.optional-dependencies&lt;/code&gt;).&#10;Users opt-in only if they need that specific capability.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Avoid Pinning (Mostly):&lt;/strong&gt; As mentioned before, hard version pins (&lt;code&gt;==1.2.3&lt;/code&gt;) in libraries are usually harmful.&#10;Minimum versions (&lt;code&gt;&amp;gt;=1.2&lt;/code&gt;) or safe upper bounds (&lt;code&gt;&amp;lt;2.0&lt;/code&gt;) are preferred.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;License Awareness:&lt;/strong&gt; The license of a dependency matters immensely, especially regarding compatibility and obligations (&lt;a href="https://www.nijho.lt/post/gpl/"&gt;GPL considerations&lt;/a&gt;).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The guiding principle is: &lt;strong&gt;Do not cause pain for your users.&lt;/strong&gt;&#10;A library should integrate smoothly into diverse environments.&lt;/p&gt;&#10;&lt;h2 id="2-the-library-author-hat--as-dependency-producer-the-paradox"&gt;2. The Library Author Hat 🎩 (as Dependency Producer): The Paradox &lt;a class="anchor" href="#2-the-library-author-hat--as-dependency-producer-the-paradox" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s where the &amp;ldquo;hypocrisy&amp;rdquo; might seem to surface.&#10;While I&amp;rsquo;m cautious about &lt;em&gt;consuming&lt;/em&gt; dependencies for my libraries, I am also the author of over 30 packages.&#10;I invest time and effort into these tools, naturally hoping they prove useful and that others &lt;em&gt;will adopt them&lt;/em&gt; – making &lt;em&gt;my&lt;/em&gt; libraries dependencies in &lt;em&gt;their&lt;/em&gt; projects.&lt;/p&gt;&#10;&lt;p&gt;Is it hypocritical to be wary of adding dependencies while simultaneously creating them for others?&#10;Perhaps slightly, but it&amp;rsquo;s also the nature of contributing to an ecosystem.&#10;I try to handle this by strictly following the rules from Section 1 for my own libraries.&#10;If I expect others to rely on my code, I feel responsible to make that dependency as&#10;low-risk and stable as possible.&#10;I want my libraries to be the kind of well-behaved, minimal-friction dependencies&#10;that I myself would be comfortable adding.&lt;/p&gt;&#10;&lt;h2 id="3-the-application-developer--contributor-hat--practicality-is-key"&gt;3. The Application Developer / Contributor Hat 🧢: Practicality is Key &lt;a class="anchor" href="#3-the-application-developer--contributor-hat--practicality-is-key" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When the context shifts to building an application – like a command-line tool, a web service, or a personal project – my rules become &lt;em&gt;much&lt;/em&gt; less strict.&#10;The key differences are isolation (keeping things separate) and how far the effects reach.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Isolation Reduces Risk:&lt;/strong&gt; Applications often run in controlled, isolated environments.&#10;&lt;a href="https://www.docker.com/" target="_blank" rel="noopener"&gt;&lt;code&gt;Docker&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;conda&lt;/code&gt;/&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;&lt;code&gt;micromamba&lt;/code&gt;&lt;/a&gt; (&lt;a href="https://www.nijho.lt/post/python-environments/"&gt;Python Environment Jungle post&lt;/a&gt;), or simple &lt;code&gt;venv&lt;/code&gt;s contain the dependencies.&#10;A conflict within the application&amp;rsquo;s environment doesn&amp;rsquo;t break the user&amp;rsquo;s entire system or other unrelated projects.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CLIs as a Prime Example:&lt;/strong&gt; Many tools I&amp;rsquo;ve built are CLIs (like &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; or helpers within my &lt;a href="https://github.com/basnijholt/dotfiles" target="_blank" rel="noopener"&gt;&lt;code&gt;dotfiles&lt;/code&gt;&lt;/a&gt;).&#10;These might have tens of dependencies.&#10;Because they are installed into isolated environments (often via &lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://pipx.pypa.io/stable/" target="_blank" rel="noopener"&gt;&lt;code&gt;pipx&lt;/code&gt;&lt;/a&gt;, or within a dedicated &lt;code&gt;conda&lt;/code&gt; env), this complexity is okay.&#10;The risk is contained.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Developer Tools Welcome:&lt;/strong&gt; Productivity boosters like &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;&lt;code&gt;ruff&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;&lt;code&gt;mypy&lt;/code&gt;&lt;/a&gt;, etc., are fair game as development dependencies (&lt;a href="https://www.nijho.lt/post/best-python-dev-tooling/"&gt;My Python Project Toolkit&lt;/a&gt;).&#10;They don&amp;rsquo;t impact the end-user runtime (if packaged properly), so I am much more lenient with dev tools.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Focus on Functionality:&lt;/strong&gt; The goal is often to deliver features or solve a problem efficiently.&#10;Leveraging existing libraries, even those with dependencies, is usually faster and more robust than reimplementing complex logic.&#10;It usually makes more sense to use existing libraries.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In this context, the focus changes from &lt;em&gt;minimizing external impact&lt;/em&gt; (libraries) to &lt;em&gt;maximizing internal capability and maintainability&lt;/em&gt; within a controlled scope (applications).&lt;/p&gt;&#10;&lt;h2 id="4-conclusion-not-hypocrisy-but-context-driven-strategy-"&gt;4. Conclusion: Not Hypocrisy, but Context-Driven Strategy 🙏 &lt;a class="anchor" href="#4-conclusion-not-hypocrisy-but-context-driven-strategy-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach to dependencies &lt;em&gt;is&lt;/em&gt; &amp;ldquo;it depends on the situation&amp;rdquo;.&#10;I&amp;rsquo;m stricter about dependencies for my libraries than for my applications or personal tools.&#10;I am cautious about adopting dependencies while actively producing them for others.&lt;/p&gt;&#10;&lt;p&gt;But I don&amp;rsquo;t see this as hypocrisy.&#10;It&amp;rsquo;s a practical approach based on different situations and goals:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Libraries (as Consumer):&lt;/strong&gt; Focus on keeping the software community stable and making things easy for users. Be conservative.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Libraries (as Producer):&lt;/strong&gt; Create software responsibly, following the same high standards you expect from others.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Applications:&lt;/strong&gt; Focus on adding features and developing quickly within separate environments. Be pragmatic.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Understanding the different risks and goals for each situation helps make good decisions about dependencies.&#10;It&amp;rsquo;s about choosing the right trade-offs for the specific situation, whether you&amp;rsquo;re building a foundational block for the community or a specific tool for a controlled environment.&lt;/p&gt;&#10;&lt;p&gt;How do you handle dependencies across different types of projects?&#10;Do you find yourself navigating similar questions?&#10;Share your thoughts!&lt;/p&gt;&#10;</description></item><item><title>Functions over classes? Why I prefer a simpler, functional style in Python 🐍</title><link>https://www.nijho.lt/post/functional-python/</link><pubDate>Mon, 14 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/functional-python/</guid><description>&lt;p&gt;Over years of building software, from scientific simulation tools like &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;&lt;code&gt;Adaptive&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; to package management packages like &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, I&amp;rsquo;ve found myself preferring a certain coding style: using small, focused functions more than large classes with complex internal state and deep inheritance hierarchies.&#10;This isn&amp;rsquo;t about dogma, but about a pragmatic approach that, in my experience, leads to code that is easier to understand, test, and maintain.&lt;/p&gt;&#10;&lt;p&gt;This post outlines why I lean towards this functional-inspired style in my Python projects, while still recognizing where classes are the right tool.&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="#1-taming-complexity-avoiding-the-over-engineering-trap-"&gt;1. Taming Complexity: Avoiding the Over-Engineering Trap 🧠&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-power-of-small-focused-functions-"&gt;2. The Power of Small, Focused Functions 🎯&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-keeping-classes-simple--using-them-where-they-fit-"&gt;3. Keeping Classes Simple &amp;amp; Using Them Where They Fit 🛠️&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-testability-advantage-"&gt;4. The Testability Advantage ✅&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-its-about-pragmatism-not-dogma-"&gt;5. It&amp;rsquo;s About Pragmatism, Not Dogma 🙏&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-taming-complexity-avoiding-the-over-engineering-trap-"&gt;1. Taming Complexity: Avoiding the Over-Engineering Trap 🧠 &lt;a class="anchor" href="#1-taming-complexity-avoiding-the-over-engineering-trap-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the biggest challenges in software development is managing complexity.&#10;As systems grow, keeping track of how different parts interact and understanding the internal state of objects can become a lot to keep track of mentally (high cognitive load).&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ve often observed a pattern, especially when developers first start using object-oriented programming: the tendency to use classes &lt;em&gt;everywhere&lt;/em&gt;.&#10;This can lead to over-engineering – introducing many layers of inheritance, abstract base classes with only a single implementation, and generally creating complexity when simplicity would suffice.&#10;Thinking you&amp;rsquo;re being clever by building complex class structures can sometimes just be a form of overgeneralization that makes the code harder, not easier, to follow.&lt;/p&gt;&#10;&lt;p&gt;Classes, particularly those with many methods modifying internal attributes (&lt;code&gt;self.x&lt;/code&gt;, &lt;code&gt;self.y&lt;/code&gt;, etc.), require you to constantly hold their state in your head.&#10;When you call a method &lt;code&gt;object.do_something()&lt;/code&gt;, the outcome might depend not just on the arguments you pass, but also on the object&amp;rsquo;s history – what methods were called before, and in what order?&#10;This hidden state can make reasoning about the code&amp;rsquo;s behavior difficult and debugging tricky.&lt;/p&gt;&#10;&lt;p&gt;Functions, particularly when kept small and focused, tend to be more explicit.&#10;Ideally, a function takes inputs and produces outputs, with minimal side effects (or ideally, none - making them pure functions).&#10;Its behavior is primarily determined by its arguments.&#10;This makes the code flow easier to follow: data comes in, transformation happens, data goes out.&#10;There&amp;rsquo;s less hidden context to juggle mentally.&lt;/p&gt;&#10;&lt;h2 id="2-the-power-of-small-focused-functions-"&gt;2. The Power of Small, Focused Functions 🎯 &lt;a class="anchor" href="#2-the-power-of-small-focused-functions-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This leads directly to the preference for small functions, each doing one thing well (adhering to the Single Responsibility Principle).&#10;When functions are concise and have a clear purpose:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Readability Improves:&lt;/strong&gt; It&amp;rsquo;s easier to understand what a small, well-named function does quickly.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reusability Increases:&lt;/strong&gt; Small, focused functions are more likely to be reusable in different parts of the codebase or even in other projects.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Refactoring is Simpler:&lt;/strong&gt; Modifying or replacing a small unit of logic is less risky than altering a large, complex method within a class.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This philosophy is something I actively apply.&#10;Looking at the codebase for &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, for example, you&amp;rsquo;ll find many utility functions in modules like &lt;code&gt;_dependencies_parsing.py&lt;/code&gt; or &lt;code&gt;_conda_env.py&lt;/code&gt;, each handling a specific, well-defined task.&lt;/p&gt;&#10;&lt;h2 id="3-keeping-classes-simple--using-them-where-they-fit-"&gt;3. Keeping Classes Simple &amp;amp; Using Them Where They Fit 🛠️ &lt;a class="anchor" href="#3-keeping-classes-simple--using-them-where-they-fit-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;So, where do classes fit in this picture?&lt;/p&gt;&#10;&lt;p&gt;Firstly, as &lt;strong&gt;simple data structures&lt;/strong&gt; – think &lt;code&gt;NamedTuple&lt;/code&gt;s or &lt;code&gt;dataclasses&lt;/code&gt; (preferably with &lt;code&gt;frozen=True&lt;/code&gt;).&#10;Their main role is to group related data attributes together, providing a clear structure for passing information around.&#10;They hold data, but don&amp;rsquo;t typically manage complex state transitions.&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;# Example of a simple data class from unidep&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&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="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Use frozen=True for immutability&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;class&lt;/span&gt; &lt;span class="nc"&gt;Spec&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;which&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;conda&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&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;# ... other simple attributes ...&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;# Used to pass structured data, not to manage complex state&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;process_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Spec&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;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="c1"&gt;# ... function logic using spec data ...&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;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Secondly, and importantly, classes are excellent for &lt;strong&gt;defining user-facing APIs&lt;/strong&gt;.&#10;For the end-user of a library, having a single object with easily discoverable methods (&lt;code&gt;object.&amp;lt;tab&amp;gt;&lt;/code&gt;) is often a great user experience.&lt;/p&gt;&#10;&lt;p&gt;A good example from my own work is the &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; package.&#10;It provides two main classes, &lt;code&gt;Pipeline&lt;/code&gt; and &lt;code&gt;PipeFunc&lt;/code&gt;, which encapsulate the core functionality.&#10;Users interact primarily with these objects and their methods.&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;# Simplified example from pipefunc&amp;#39;s user perspective&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pipefunc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pipeline&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;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pipeline&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="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&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="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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;output_node&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;However, &lt;strong&gt;inside&lt;/strong&gt; the methods of these API classes (&lt;code&gt;Pipeline.run&lt;/code&gt;, &lt;code&gt;Pipeline.add&lt;/code&gt;, etc.), the actual logic is often delegated to smaller, internal functions.&#10;The class acts as a clean interface or facade, orchestrating calls to these more focused, functional units.&#10;This gives the user a nice API while keeping the internal implementation modular and testable.&lt;/p&gt;&#10;&lt;p&gt;I also actively avoid deep inheritance hierarchies.&#10;Composition (having objects contain other objects) often provides a more flexible and explicit way to combine functionality without the tight coupling and complexity inheritance can introduce.&lt;/p&gt;&#10;&lt;h2 id="4-the-testability-advantage-"&gt;4. The Testability Advantage ✅ &lt;a class="anchor" href="#4-the-testability-advantage-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the most significant practical benefits of prioritizing functions and simple classes is testability.&#10;Small, pure (or mostly pure) functions are incredibly easy to test:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Provide known inputs.&lt;/li&gt;&#10;&lt;li&gt;Assert expected outputs.&lt;/li&gt;&#10;&lt;li&gt;Minimal setup or mocking is required because there&amp;rsquo;s no complex internal state to manage.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Even when testing the methods of the API classes (like &lt;code&gt;Pipeline&lt;/code&gt; in &lt;code&gt;pipefunc&lt;/code&gt;), the fact that they often delegate to well-tested internal functions simplifies the process.&#10;You focus on testing the orchestration and interaction logic at the class level, knowing the underlying functional units are already verified.&lt;/p&gt;&#10;&lt;p&gt;Testing classes with intricate internal state inherently involves more setup (instantiating the object, getting it into the right state before calling the method under test) and potentially more complex assertions to verify state changes.&#10;Aiming for more functional units significantly simplifies the testing process.&lt;/p&gt;&#10;&lt;h2 id="5-its-about-pragmatism-not-dogma-"&gt;5. It&amp;rsquo;s About Pragmatism, Not Dogma 🙏 &lt;a class="anchor" href="#5-its-about-pragmatism-not-dogma-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t a wholesale rejection of object-oriented programming.&#10;It&amp;rsquo;s about choosing the right tool for the job and prioritizing clarity, testability, and maintainability.&#10;Classes are fantastic tools, especially for building clear APIs and structuring data.&#10;Deep inheritance and complex state management, however, should be used carefully, not just because the language allows it.&lt;/p&gt;&#10;&lt;p&gt;For much of the work I do – building libraries, tools, and performing data analysis or simulations – I&amp;rsquo;ve found that prioritizing functions, keeping API classes focused, minimizing internal state, and avoiding deep inheritance leads to code that &lt;em&gt;I&lt;/em&gt; find easier to write, understand, debug, and maintain in the long run.&#10;The reduction in cognitive load and the ease of testing are strong points for how I work.&lt;/p&gt;&#10;&lt;p&gt;If you often find yourself struggling with complex state management or tangled inheritance trees, perhaps consider if a simpler, more functional internal structure, potentially fronted by a clean class-based API, might bring some clarity and simplicity to your projects too.&lt;/p&gt;&#10;</description></item><item><title>Vibe coding worked—until the 15‑hour cleanup</title><link>https://www.nijho.lt/post/vibe-coding/</link><pubDate>Fri, 21 Mar 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/vibe-coding/</guid><description>&lt;h2 id="i-tried-vibe-coding-for-3-hours-my-application-worked-but-then-i-spent-15-hours-fixing-it"&gt;I tried &amp;ldquo;vibe coding&amp;rdquo; for 3 hours, my application worked, but then I spent 15 hours fixing it &lt;a class="anchor" href="#i-tried-vibe-coding-for-3-hours-my-application-worked-but-then-i-spent-15-hours-fixing-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;That&amp;rsquo;s not an exaggeration—it&amp;rsquo;s really what happened.&lt;/p&gt;&#10;&lt;p&gt;Recently, I tried &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Vibe_coding" target="_blank" rel="noopener"&gt;Vibe coding&lt;/a&gt;,&amp;rdquo; a term coined by Andrej Karpathy, a prominent AI researcher, co-founder of OpenAI, and former AI lead at Tesla.&#10;In &lt;a href="https://x.com/karpathy/status/1886192184808149383" target="_blank" rel="noopener"&gt;his viral tweet&lt;/a&gt; from February 2025, Karpathy described vibe coding as a programming approach where you &amp;ldquo;fully give in to the vibes, embrace exponentials, and forget that the code even exists.&amp;rdquo;&#10;It&amp;rsquo;s essentially about letting AI handle the coding based on natural language prompts without closely reviewing the generated code.&lt;/p&gt;&#10;&lt;p&gt;Intrigued by the hype, I decided to experiment with vibe coding using &lt;a href="https://www.cursor.com/" target="_blank" rel="noopener"&gt;Cursor&lt;/a&gt; on a new Python project I called &lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;dotbins&lt;/a&gt; (&lt;a href="../dotbins"&gt;check out my detailed write-up here&lt;/a&gt;).&amp;quot;&#10;&lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;Dotbins&lt;/a&gt; manages CLI tool binaries within Git repositories, automatically downloading the correct binary from GitHub for the user&amp;rsquo;s platform and architecture, significantly simplifying my dotfiles and bootstrapping process.&#10;I am super excited about this project but it is not what this post is about.&lt;/p&gt;&#10;&lt;p&gt;Initially, it was fun to get something working so quickly.&#10;Within just three hours, I had a functioning prototype—without carefully examining the code.&#10;But I had unknowingly created a monster—a true &lt;a href="https://en.wikipedia.org/wiki/Spaghetti_code" target="_blank" rel="noopener"&gt;spaghetti&lt;/a&gt; monster! 🍝&lt;/p&gt;&#10;&lt;p&gt;Once I started reviewing the AI-generated code, I realized it was totally incomprehensible.&#10;The AI had implemented the most complicated and roundabout ways to achieve simple tasks.&#10;It introduced redundant checks for impossible conditions, irrelevant error handling, and execution paths that would never trigger.&#10;The code worked superficially, but beneath the surface, it was chaotic and nearly impossible to understand or maintain.&lt;/p&gt;&#10;&lt;p&gt;This realization turned three hours of fun into fifteen hours of meticulous debugging, refactoring, and cleaning up the mess.&#10;Essentially, I had to rewrite the entire project.&lt;/p&gt;&#10;&lt;p&gt;My main takeaway: AI-assisted coding, or &amp;ldquo;vibe coding,&amp;rdquo; is incredibly powerful for quick prototyping but dangerous when used without oversight.&#10;According to &lt;a href="https://www.youtube.com/watch?v=IACHfKmZMr8" target="_blank" rel="noopener"&gt;this recent Y Combinator discussion&lt;/a&gt;, many startup founders are building products almost entirely with AI-generated code, &amp;ldquo;vibing&amp;rdquo; the code, meaning they let the AI write the code and don&amp;rsquo;t review it.&#10;For writing a couple of hundred lines of code it likely works pretty well, but for anything more complex, it&amp;rsquo;s a recipe for disaster.&#10;Personally, I would never invest in, build upon, or use such products—they&amp;rsquo;re essentially constructing technical prisons for themselves.&lt;/p&gt;&#10;&lt;p&gt;AI is fantastic for inspiration, quick prototyping, writing code, and refactoring—but letting it run wild without careful human review can quickly lead to severe technical debt and hidden bugs.&lt;/p&gt;&#10;&lt;p&gt;Thanks for reading!&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>Python environments that stuck: micromamba and uv 🐍</title><link>https://www.nijho.lt/post/python-environments/</link><pubDate>Fri, 03 Jan 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/python-environments/</guid><description>&lt;h2 id="1-introduction-"&gt;1. Introduction 🚀 &lt;a class="anchor" href="#1-introduction-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Like many Python developers, I&amp;rsquo;ve spent considerable time trying different environment management tools to find what works best for me.&lt;/p&gt;&#10;&lt;p&gt;After exploring various options from virtualenv to Conda, I found that Conda-based solutions fit my needs well - mainly because they handle non-Python dependencies without much hassle.&lt;/p&gt;&#10;&lt;p&gt;While Conda itself can be slow when setting up environments, tools like Mamba and Pixi provide the same functionality with better performance.&lt;/p&gt;&#10;&lt;p&gt;In this post, I&amp;rsquo;ll share my experience with different Python environment tools and provide practical commands for setting up Python environments.&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;TL;DR, I use &lt;code&gt;micromamba&lt;/code&gt; for projects with non-Python dependencies and &lt;code&gt;uv&lt;/code&gt; for projects without them.&#10;This approach works well for me, you own mileage may vary.&lt;/div&gt;&lt;/aside&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="#1-introduction-"&gt;1. Introduction 🚀&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-alternatives-"&gt;2. The Alternatives 🌟&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#-pyenv"&gt;🐍 Pyenv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-pipenv"&gt;📦 Pipenv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-conda"&gt;🌐 Conda&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-mamba"&gt;🚀 Mamba&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-micromamba"&gt;🌌 Micromamba&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-docker"&gt;🐳 Docker&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-venv"&gt;🏞️ Venv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-virtualenv"&gt;🌐 Virtualenv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-homebrew"&gt;🍺 Homebrew&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-system-package-managers"&gt;💻 System package managers&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-pixi"&gt;🌟 Pixi&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-uv"&gt;⚡ uv&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-conclusion-"&gt;3. Conclusion 🏁&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-further-reading-"&gt;4. Further Reading 📚&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="2-the-alternatives-"&gt;2. The Alternatives 🌟 &lt;a class="anchor" href="#2-the-alternatives-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before we start look at this comparison tables for the most popular Python environment management tools.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;I had to split the tables into two because of the number of columns.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Table 1: Python Environment Management Tools&lt;/em&gt;&lt;/strong&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;Tool&lt;/th&gt;&#10;&lt;th&gt;Key Features&lt;/th&gt;&#10;&lt;th&gt;Native/3rd Party&lt;/th&gt;&#10;&lt;th&gt;Non-Python Deps&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/pyenv/pyenv" target="_blank" rel="noopener"&gt;Pyenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Python version management&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://pipenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Pipenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Package &amp;amp; env management&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.conda.io/en/latest/" target="_blank" rel="noopener"&gt;Conda&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Package &amp;amp; env management&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Mamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fast Conda replacement&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;Micromamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Lightweight Mamba&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://www.docker.com/" target="_blank" rel="noopener"&gt;Docker&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Containerization&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.python.org/3/library/venv.html" target="_blank" rel="noopener"&gt;Venv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Built-in env management&lt;/td&gt;&#10;&lt;td&gt;Native&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://virtualenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Virtualenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Env management (pre-3.3)&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;Homebrew&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;macOS/Linux package manager&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://prefix.dev/docs/pixi/overview" target="_blank" rel="noopener"&gt;Pixi&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fast Conda alternative&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener"&gt;uv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fast pip and venv replacement&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;System Package Managers&lt;/td&gt;&#10;&lt;td&gt;OS-based package management&lt;/td&gt;&#10;&lt;td&gt;Native&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Table 2: Python Environment Management Tools&lt;/em&gt;&lt;/strong&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;Tool&lt;/th&gt;&#10;&lt;th&gt;Ease of Use&lt;/th&gt;&#10;&lt;th&gt;Isolation&lt;/th&gt;&#10;&lt;th&gt;Cross-Platform Support&lt;/th&gt;&#10;&lt;th&gt;Community &amp;amp; Support&lt;/th&gt;&#10;&lt;th&gt;Performance&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/pyenv/pyenv" target="_blank" rel="noopener"&gt;Pyenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Medium&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://pipenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Pipenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.conda.io/en/latest/" target="_blank" rel="noopener"&gt;Conda&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Average&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Mamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;Micromamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Medium&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://www.docker.com/" target="_blank" rel="noopener"&gt;Docker&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Hard&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.python.org/3/library/venv.html" target="_blank" rel="noopener"&gt;Venv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://virtualenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Virtualenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Medium&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;Homebrew&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;td&gt;macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://prefix.dev/docs/pixi/overview" target="_blank" rel="noopener"&gt;Pixi&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener"&gt;uv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Very Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;System Package Managers&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;td&gt;Depends on OS&lt;/td&gt;&#10;&lt;td&gt;Depends on OS&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Now that we&amp;rsquo;ve seen the alternatives, let&amp;rsquo;s dive into setting up environments with the help of Conda and Mamba.&lt;/p&gt;&#10;&lt;h3 id="-pyenv"&gt;🐍 Pyenv &lt;a class="anchor" href="#-pyenv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A popular tool that enables you to easily manage multiple Python versions on a single system.&#10;It allows you to switch between different Python versions without interfering with system-level installations.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install pyenv:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS: &lt;code&gt;brew install pyenv&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Ubuntu: &lt;code&gt;curl https://pyenv.run | bash&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Add the following lines to your shell configuration file (e.g., &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.zshrc&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="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;/.pyenv/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;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pyenv init -&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Restart your shell or run &lt;code&gt;source ~/.bashrc&lt;/code&gt; (or &lt;code&gt;source ~/.zshrc&lt;/code&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install a specific Python version: &lt;code&gt;pyenv install 3.8.5&lt;/code&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Set the Python version for your project: &lt;code&gt;pyenv local 3.8.5&lt;/code&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-pipenv"&gt;📦 Pipenv &lt;a class="anchor" href="#-pipenv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A package management and virtual environment management tool that combines the functionality of pip and virtualenv.&#10;It streamlines the process of installing and managing packages within isolated environments.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install pipenv: &lt;code&gt;pip install --user pipenv&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Change to your project directory: &lt;code&gt;cd my_project&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Initialize a new virtual environment: &lt;code&gt;pipenv --python 3.8&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;pipenv install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment: &lt;code&gt;pipenv shell&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-conda"&gt;🌐 Conda &lt;a class="anchor" href="#-conda" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A cross-platform package manager and environment management tool, often used in conjunction with Anaconda and Miniconda.&#10;Conda simplifies the installation and management of packages and dependencies, especially for data science and machine learning projects.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Download and install Miniconda: &lt;a href="https://github.com/conda-forge/miniforge#miniforge3" target="_blank" rel="noopener"&gt;https://github.com/conda-forge/miniforge#miniforge3&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new environment: &lt;code&gt;conda create --name my_env python=3.8&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment: &lt;code&gt;conda activate my_env&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;conda install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-mamba"&gt;🚀 Mamba &lt;a class="anchor" href="#-mamba" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Mamba is a fast, drop-in replacement for Conda, designed to resolve and install packages more quickly.&#10;It uses the same Conda repositories and environment files, making it easy to switch between the two.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install Mambaforge with Mamba pre-installed:&lt;/p&gt;&#10;&lt;p&gt;Download and install Mambaforge with Mamba for your platform: &lt;a href="https://github.com/conda-forge/miniforge#mambaforge" target="_blank" rel="noopener"&gt;https://github.com/conda-forge/miniforge#mambaforge&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Alternatively, you can install Mamba within an existing Conda environment:&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;conda install -c conda-forge mamba&#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 new environment with Mamba:&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;mamba create --name my_env &lt;span class="nv"&gt;python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.8&#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;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;mamba activate my_env&#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;Install packages using Mamba:&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;mamba install &amp;lt;package_name&amp;gt; -c conda-forge&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;By using Mamba instead of Conda, you can significantly speed up environment resolution and package installation while still benefiting from the Conda ecosystem.&lt;/p&gt;&#10;&lt;h3 id="-micromamba"&gt;🌌 Micromamba &lt;a class="anchor" href="#-micromamba" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Micromamba is a lightweight and fast alternative to the Mamba package manager.&#10;It is a statically linked C++ executable that doesn&amp;rsquo;t require a base environment or come with a default Python version.&#10;It supports a subset of Mamba and Conda commands and is designed to resolve and install packages quickly.&#10;For those who need Conda features, you can install Conda using Micromamba with &lt;code&gt;micromamba install conda&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Download the appropriate Micromamba binary for your platform from &lt;a href="https://github.com/mamba-org/mamba/releases" target="_blank" rel="noopener"&gt;the official repository&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;Make the binary executable and move it to a directory in your &lt;code&gt;PATH&lt;/code&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;chmod +x micromamba mv micromamba /usr/local/bin/&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new environment with Micromamba:&#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;micromamba create -n my_env python -c conda-forge&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#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;micromamba activate my_env&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Install packages using Micromamba:&#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;micromamba install &amp;lt;package_name&amp;gt; -c conda-forge&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;If you need Conda&amp;rsquo;s features, you can install Conda within your Micromamba environment:&#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;micromamba install conda&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;For example, if you need nb_conda_kernels to use your Micromamba environment in JupyterLab:&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;micromamba install conda nb_conda_kernels&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And you are set up!&lt;/p&gt;&#10;&lt;p&gt;Micromamba is the perfect solution for those who want a fast and lightweight package manager while still being able to access Conda&amp;rsquo;s features when needed.&lt;/p&gt;&#10;&lt;h3 id="-docker"&gt;🐳 Docker &lt;a class="anchor" href="#-docker" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A containerization platform that allows you to create lightweight, portable environments called containers.&#10;With Docker, you can package your Python application along with its dependencies, ensuring consistent execution across different systems.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install Docker: &lt;a href="https://docs.docker.com/get-docker/" target="_blank" rel="noopener"&gt;https://docs.docker.com/get-docker/&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt; in your project directory with the following contents:&#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;FROM python:3.11&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WORKDIR /app&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY requirements.txt .&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RUN pip install --no-cache-dir -r requirements.txt&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY . .&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CMD &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;python&amp;#34;&lt;/span&gt;, &lt;span class="s2"&gt;&amp;#34;your_script.py&amp;#34;&lt;/span&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;/li&gt;&#10;&lt;li&gt;Build the Docker image: &lt;code&gt;docker build -t my_project .&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Run the Docker container: &lt;code&gt;docker run my_project&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-venv"&gt;🏞️ Venv &lt;a class="anchor" href="#-venv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A built-in Python module available since Python 3.3, which allows you to create lightweight virtual environments.&#10;Venv simplifies the process of creating isolated environments for different projects, ensuring that package dependencies don&amp;rsquo;t conflict with each other.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Create a new virtual environment: &lt;code&gt;python3 -m venv my_env&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS/Linux: &lt;code&gt;source my_env/bin/activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Windows: &lt;code&gt;my_envScriptsactivate&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;pip install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-virtualenv"&gt;🌐 Virtualenv &lt;a class="anchor" href="#-virtualenv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Virtualenv is a third-party Python environment management tool that predates the built-in &lt;code&gt;venv&lt;/code&gt; module. It allows you to create isolated environments for different projects, ensuring that package dependencies don&amp;rsquo;t conflict with each other. Virtualenv is compatible with Python 2.7 and later versions, whereas &lt;code&gt;venv&lt;/code&gt; is only available since Python 3.3.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install virtualenv: &lt;code&gt;pip install virtualenv&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new virtual environment: &lt;code&gt;virtualenv my_env&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS/Linux: &lt;code&gt;source my_env/bin/activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Windows: &lt;code&gt;my_env\Scripts\activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;pip install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-homebrew"&gt;🍺 Homebrew &lt;a class="anchor" href="#-homebrew" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Homebrew is a popular package manager for macOS and Linux that simplifies the installation and management of software. Though it is not a dedicated Python environment management tool, it does offer support for installing and managing multiple Python versions alongside other software.&lt;/p&gt;&#10;&lt;p&gt;Homebrew can be useful for installing and managing Python versions for system-level usage, but it&amp;rsquo;s not the best choice for managing isolated project-specific dependencies. For project-specific dependency management, it&amp;rsquo;s recommended to use other tools like venv, virtualenv, or Conda in conjunction with Homebrew-installed Python.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install Homebrew (if not already installed):&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS: Follow the instructions at &lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;https://brew.sh&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Linux: Follow the instructions at &lt;a href="https://docs.brew.sh/Homebrew-on-Linux" target="_blank" rel="noopener"&gt;https://docs.brew.sh/Homebrew-on-Linux&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install a specific Python version:&#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;brew install python@3.11&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Add the installed Python version to your &lt;code&gt;PATH&lt;/code&gt; by adding the following line to your shell configuration file (e.g., &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.zshrc&lt;/code&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;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/usr/local/opt/python@3.11/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;Restart your shell or run &lt;code&gt;source ~/.bashrc&lt;/code&gt; (or &lt;code&gt;source ~/.zshrc&lt;/code&gt;) to apply the changes.&lt;/li&gt;&#10;&lt;li&gt;Verify the Python version: &lt;code&gt;python3.11 --version&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Keep in mind that Homebrew is mainly for installing and managing software at the system level. To manage project-specific dependencies and create isolated environments, combine Homebrew with other tools like venv or virtualenv for better dependency management.&lt;/p&gt;&#10;&lt;h3 id="-system-package-managers"&gt;💻 System package managers &lt;a class="anchor" href="#-system-package-managers" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Some operating systems have their own package managers (e.g., apt for Ubuntu, pacman for Arch Linux, or Homebrew for macOS) that can be used to install and manage Python environments.&#10;However, using system package managers may lead to conflicts with other system packages and is generally not recommended for managing project-specific dependencies.&lt;/p&gt;&#10;&lt;p&gt;I will skip the setup instructions for this one, as it is just not recommended to do this!&lt;/p&gt;&#10;&lt;h3 id="-pixi"&gt;🌟 Pixi &lt;a class="anchor" href="#-pixi" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Pixi is a new package manager built in Rust that uses Conda-forge packages but aims to be significantly faster and simpler than Conda.&#10;It&amp;rsquo;s developed by prefix.dev (the team behind Mamba) and focuses on providing a more streamlined experience.&#10;It is inspired by tools like Cargo, nix, and npm and aims to provide a modern package management experience for Python developers.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install pixi following instructions at &lt;a href="https://prefix.dev/docs/pixi/installation" target="_blank" rel="noopener"&gt;prefix.dev&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Initialize a new project:&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;pixi init my-project&#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&#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 dependencies:&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;pixi add python numpy pandas&#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;Run Python in 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;pixi run python&#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 and run tasks (defined in pixi.toml):&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;# Add a task&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi task add hello &lt;span class="s2"&gt;&amp;#34;python hello_world.py&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;# Run the task&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi run hello&#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;Start an interactive shell in 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;pixi shell&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Pixi can also be used as a global installation tool:&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;# Install tools globally&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi global install starship&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi global install fish&#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 a named environment with multiple packages&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi global install --environment data-science-env &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; --expose python --expose jupyter &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; python jupyter numpy pandas&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Key features:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Fast package resolution and installation&lt;/li&gt;&#10;&lt;li&gt;Uses Conda-forge packages&lt;/li&gt;&#10;&lt;li&gt;Built-in task runner&lt;/li&gt;&#10;&lt;li&gt;Global package installation support&lt;/li&gt;&#10;&lt;li&gt;GitHub Actions integration with automatic caching&lt;/li&gt;&#10;&lt;li&gt;Simple and intuitive CLI&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="-uv"&gt;⚡ uv &lt;a class="anchor" href="#-uv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;uv is an extremely fast Python package installer and resolver written in Rust.&#10;It can be used as a drop-in replacement for pip and can generate virtual environments.&#10;While it is quite new it is already being used by many developers due to its speed and ease of use.&#10;Besides being fast, it is awesome because it is a single binary that can be used to also bootstrap Python itself.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/getting-started/installation/" target="_blank" rel="noopener"&gt;Install uv&lt;/a&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;curl -LsSf https://astral.sh/uv/install.sh &lt;span class="p"&gt;|&lt;/span&gt; sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new virtual environment:&#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;uv venv&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#10;&lt;ul&gt;&#10;&lt;li&gt;Unix/macOS: &lt;code&gt;source .venv/bin/activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Windows: &lt;code&gt;.venv\Scripts\activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install packages (much faster than pip):&#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;uv pip install numpy pandas&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;It will automatically maintain a &lt;code&gt;uv.lock&lt;/code&gt; file to ensure deterministic builds.&#10;Install this lock file with &lt;code&gt;uv sync&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Key features of uv:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Up to 10x faster than pip&lt;/li&gt;&#10;&lt;li&gt;Built-in virtual environment support&lt;/li&gt;&#10;&lt;li&gt;Compatible with pip&amp;rsquo;s CLI interface&lt;/li&gt;&#10;&lt;li&gt;Global package caching&lt;/li&gt;&#10;&lt;li&gt;Deterministic builds&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;There is much more to uv, so check out the &lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt; for more details.&lt;/p&gt;&#10;&lt;h2 id="3-conclusion-"&gt;3. Conclusion 🏁 &lt;a class="anchor" href="#3-conclusion-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The choice you make for managing your Python environments ultimately depends on your use case.&#10;If you don&amp;rsquo;t have to frequently deal with non-Python dependencies, I would recommend using &lt;code&gt;uv&lt;/code&gt; for its speed and simplicity.&#10;However, because I often work with projects that require non-Python dependencies, I&amp;rsquo;ve found that Micromamba is the perfect fit for me.&lt;/p&gt;&#10;&lt;p&gt;That being said, each tool has its own strengths and weaknesses, and it&amp;rsquo;s important to choose the one that works best for your specific needs.&#10;I hope this journey through Python environment management has been helpful and informative.&#10;Good luck in finding your perfect Python environment workflow! 🐍&lt;/p&gt;&#10;&lt;h2 id="4-further-reading-"&gt;4. Further Reading 📚 &lt;a class="anchor" href="#4-further-reading-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here are some resources for further exploration:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/pyenv/pyenv" target="_blank" rel="noopener"&gt;&lt;strong&gt;Pyenv&lt;/strong&gt;&lt;/a&gt;: Pyenv GitHub repository and documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://pipenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Pipenv&lt;/strong&gt;&lt;/a&gt;: Pipenv documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.conda.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Conda&lt;/strong&gt;&lt;/a&gt;: Conda documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Mamba&lt;/strong&gt;&lt;/a&gt;: Mamba documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;&lt;strong&gt;Micromamba&lt;/strong&gt;&lt;/a&gt;: Micromamba documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.python.org/3/library/venv.html" target="_blank" rel="noopener"&gt;&lt;strong&gt;venv&lt;/strong&gt;&lt;/a&gt;: venv documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://virtualenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;virtualenv&lt;/strong&gt;&lt;/a&gt;: virtualenv documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.anaconda.com/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Anaconda&lt;/strong&gt;&lt;/a&gt;: Anaconda homepage&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.conda.io/en/latest/miniconda.html" target="_blank" rel="noopener"&gt;&lt;strong&gt;Miniconda&lt;/strong&gt;&lt;/a&gt;: Miniconda documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Homebrew&lt;/strong&gt;&lt;/a&gt;: Homebrew homepage&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://prefix.dev/docs/pixi/overview" target="_blank" rel="noopener"&gt;&lt;strong&gt;Pixi&lt;/strong&gt;&lt;/a&gt;: Pixi documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;&lt;strong&gt;uv&lt;/strong&gt;&lt;/a&gt;: uv documentation&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Discovering Git worktrees: a handy tool for multitasking 📚</title><link>https://www.nijho.lt/post/git-worktree/</link><pubDate>Thu, 19 Sep 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/git-worktree/</guid><description>&lt;p&gt;Recently, I discovered a game-changer in Git that has streamlined my workflow: &lt;strong&gt;worktrees&lt;/strong&gt;.&#10;This feature allows developers to handle multiple branches of a single repository simultaneously, eliminating the need for constant branch-switching or multiple clones.&#10;For example, I typically want to keep the &lt;code&gt;main&lt;/code&gt; branch in one editor window and the &lt;code&gt;feature&lt;/code&gt; branch in another to easily test changes.&#10;With Git worktrees, I essentially have two separate working directories for each branch, making it easier to manage changes and switch between tasks.&lt;/p&gt;&#10;&lt;h4 id="what-is-a-git-worktree"&gt;What is a Git Worktree?&lt;/h4&gt;&#10;&lt;p&gt;A Git worktree provides additional working directories linked to the same repository.&#10;This means you can easily work on different branches in parallel without duplicating repositories, saving both space and effort.&lt;/p&gt;&#10;&lt;h4 id="benefits-of-git-worktrees"&gt;Benefits of Git Worktrees&lt;/h4&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Parallel Development&lt;/strong&gt;: Effortlessly switch between branches for urgent bug fixes or new feature developments.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: It&amp;rsquo;s lightweight, sharing the Git directory, and integrates smoothly with existing workflows.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h4 id="quick-commands"&gt;Quick Commands&lt;/h4&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Create a Worktree&lt;/strong&gt;:&lt;br&gt;&#10;&lt;code&gt;git worktree add &amp;lt;path-to-new-worktree&amp;gt; &amp;lt;branch-name&amp;gt;&lt;/code&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;List Worktrees&lt;/strong&gt;:&lt;br&gt;&#10;&lt;code&gt;git worktree list&lt;/code&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Remove a Worktree&lt;/strong&gt;:&lt;br&gt;&#10;&lt;code&gt;git worktree remove &amp;lt;path-to-worktree&amp;gt;&lt;/code&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h4 id="conclusion"&gt;Conclusion&lt;/h4&gt;&#10;&lt;p&gt;Incorporating Git worktrees into your workflow enhances productivity and organization.&#10;Whether dealing with bug fixes or new features, worktrees help maintain a cleaner environment.&lt;/p&gt;&#10;</description></item><item><title>My favorite tools that keep my Python projects sane 🧰</title><link>https://www.nijho.lt/post/best-python-dev-tooling/</link><pubDate>Sat, 22 Apr 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/best-python-dev-tooling/</guid><description>&lt;h2 id="-1-introduction"&gt;🌟 1. Introduction &lt;a class="anchor" href="#-1-introduction" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;As a passionate Python developer, I am always on the hunt for the best tools to streamline my development process and enhance the quality of my projects. Over time, I have discovered a set of tools that have become indispensable in my workflow, making my coding experience more efficient and enjoyable. Using the right developer tools for Python projects is crucial for maximizing productivity, ensuring code consistency, and maintaining high-quality code.&lt;/p&gt;&#10;&lt;p&gt;In this blog post, I will share these essential tools that I now use for every single new project. We will explore the benefits of each tool and discuss how they can help you supercharge your Python development. The tools covered in this post include &lt;a href="https://pre-commit.com/" target="_blank" rel="noopener"&gt;pre-commit&lt;/a&gt;, &lt;a href="https://black.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Black&lt;/a&gt;, &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;Ruff&lt;/a&gt;, &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Mypy&lt;/a&gt;, &lt;a href="https://versioningit.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Versioningit&lt;/a&gt;, &lt;a href="https://www.python.org/dev/peps/pep-0621/" target="_blank" rel="noopener"&gt;Pyproject.toml with PEP 621&lt;/a&gt;, &lt;a href="https://nox.thea.codes/en/stable/" target="_blank" rel="noopener"&gt;Nox&lt;/a&gt;, &lt;a href="https://docs.pytest.org/en/7.3.x/" target="_blank" rel="noopener"&gt;pytest&lt;/a&gt;, &lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener"&gt;GitHub Actions CI matrix&lt;/a&gt;, and &lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;Read the Docs&lt;/a&gt;, &lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;Sphinx&lt;/a&gt;, &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt;, and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext notebooks&lt;/a&gt; for documentation. Let&amp;rsquo;s dive in and learn how to enhance your Python projects with these powerful tools!&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="#-1-introduction"&gt;🌟 1. Introduction&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-2-pre-commit"&gt;🛠️ 2. Pre-commit&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-3-black-auto-formatting-for-python"&gt;🎨 3. Black: auto-formatting for Python&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-4-ruff-the-fastest-python-linter-and-auto-formatter"&gt;🚀 4. Ruff: the fastest Python linter and auto-formatter&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-5-mypy-static-type-checking-for-python-find-bugs-before-they-happen"&gt;🔍 5. Mypy: static type checking for Python (find bugs before they happen)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-6-nox-and-pytest-for-testing-a-powerful-combination"&gt;🧪 6. Nox and pytest for testing: a powerful combination&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-7-versioningit-version-management-with-git-tags"&gt;🏷️ 7. Versioningit: version management with Git tags&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-8-pyprojecttoml-and-pep-621-configuration-consolidation"&gt;📝 8. Pyproject.toml and PEP 621: Configuration Consolidation&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-9-github-actions-ci-matrix-testing-on-multiple-platforms-and-auto-publishing-to-pypi"&gt;🌐 9. GitHub Actions CI Matrix: Testing on Multiple Platforms and Auto Publishing to PyPI&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-10-read-the-docs-sphinx-myst-nb-and-jupytext-notebooks-for-documentation"&gt;📚 10. Read the Docs, Sphinx, MyST-NB, and Jupytext notebooks for documentation&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-11-conclusion"&gt;🎉 11. Conclusion&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;This post is based the way I develop Python projects. For full examples see &lt;a href="https://github.com/basnijholt/adaptive-scheduler/" target="_blank" rel="noopener"&gt;Adaptive Scheduler&lt;/a&gt; (full project), &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; (for a single module package), and &lt;a href="https://github.com/python-adaptive/adaptive/" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt; (for cool documentation and nox).&#10;Over time I might change my mind about some of the tools, but I will try to keep this post up-to-date.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="-2-pre-commit"&gt;🛠️ 2. Pre-commit &lt;a class="anchor" href="#-2-pre-commit" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the most common challenges developers face when working on a project, whether alone or in a team, is maintaining code quality and consistency. As projects grow, it becomes increasingly difficult to enforce coding standards and catch issues before they make their way into the codebase. This is where pre-commit comes in.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://pre-commit.com/" target="_blank" rel="noopener"&gt;Pre-commit&lt;/a&gt; is a powerful tool that helps you manage and maintain code quality by automating checks before commits. It achieves this by setting up a series of hooks that run before each commit, ensuring that only code that meets your predefined standards is committed to the repository. By catching issues early, pre-commit helps you keep your code clean and consistent, making it easier to review and maintain.&lt;/p&gt;&#10;&lt;p&gt;One of the reasons pre-commit has gained widespread adoption is its flexibility and extensibility. You can choose from a vast range of built-in hooks, such as code formatters, linters, and security checkers, or even create your own custom hooks. This allows you to tailor pre-commit to the specific needs of your project, ensuring that it enforces the exact coding standards you require.&lt;/p&gt;&#10;&lt;p&gt;Using pre-commit is beneficial for both individual developers and teams. For solo developers, it helps maintain discipline and enforces coding standards, making it easier to onboard collaborators in the future. For teams, it streamlines the review process by catching issues before they are submitted for review, reducing the time spent on addressing code inconsistencies and style violations.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s take a look at an example. Suppose your project follows the PEP 8 coding standard, and you want to ensure that all code committed to the repository adheres to these guidelines. You can configure pre-commit to run a PEP 8 linter, such as Flake8, before each commit. This way, any code that does not meet the PEP 8 standard will be flagged, and the commit will be blocked until the issues are resolved.&lt;/p&gt;&#10;&lt;p&gt;In summary, pre-commit is an invaluable tool for maintaining code quality and consistency in your Python projects. By automating checks before commits, it ensures that your codebase remains clean and adheres to your chosen standards, making your development process more efficient and enjoyable.&lt;/p&gt;&#10;&lt;p&gt;Adding pre-commit to your Python project is simple and straightforward. To get started, you&amp;rsquo;ll first need to install the pre-commit package using pip:&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 pre-commit&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, create a configuration file named &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; in your project&amp;rsquo;s root directory. This file will define the hooks you want to run before each commit. For example, to use Black for code formatting and Flake8 for linting, your configuration file might look like this:&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;repos&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;https://github.com/psf/black&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;rev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;stable&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;hooks&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;black&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;https://github.com/pycqa/flake8&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;rev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;master&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;hooks&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;flake8&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;Once you have set up the configuration file, run the following command to install the pre-commit hooks:&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;pre-commit install&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, every time you attempt to make a commit, pre-commit will run the specified hooks. If any issues are found, the commit will be blocked, and you&amp;rsquo;ll be prompted to fix the problems before trying again. To run pre-commit manually on all files in your repository, use the following 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;pre-commit run --all-files&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By incorporating pre-commit into your development process, you&amp;rsquo;ll be able to catch and fix issues before they make their way into your codebase, ensuring a consistent and high-quality Python project.&lt;/p&gt;&#10;&lt;h2 id="-3-black-auto-formatting-for-python"&gt;🎨 3. Black: auto-formatting for Python &lt;a class="anchor" href="#-3-black-auto-formatting-for-python" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;As a developer who has always been particular about coding style, I&amp;rsquo;ve had my fair share of discussions on the proper way to indent multiline statements and other formatting nuances. These conversations, although sometimes necessary, can take up valuable time that could be better spent on actual development. That&amp;rsquo;s where Black, the uncompromising code formatter, comes in and saves the day.&lt;/p&gt;&#10;&lt;p&gt;Adopting &lt;a href="https://black.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Black&lt;/a&gt; has made a significant impact on my development workflow. No more unnecessary discussions about formatting preferences; everyone on the team simply follows the rules set forth by Black. This consistency across the codebase not only makes the code more readable but also reduces the time spent on formatting debates.&lt;/p&gt;&#10;&lt;p&gt;One of the best aspects of Black is that you don&amp;rsquo;t need to worry about writing perfectly formatted code initially. You can write your code as quickly as you want, even if it&amp;rsquo;s poorly formatted, and then simply run Black to transform it into a beautifully formatted masterpiece. This can be a real time-saver, especially when working on large projects with many contributors.&lt;/p&gt;&#10;&lt;p&gt;To install Black, run the following 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;pip install black&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can then format your code by running Black in the terminal:&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;black your_file.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alternatively, you can use the VS Code command palette to run Black on the currently open file. However, the most efficient way to use Black is in combination with pre-commit, as mentioned in the previous section. This way, your code is automatically formatted every time you commit, ensuring a consistent codebase without any extra effort.&lt;/p&gt;&#10;&lt;p&gt;In summary, Black is an invaluable tool for enforcing consistent code formatting throughout your Python projects. By adopting it and integrating it with pre-commit, you can save time and focus on what truly matters: writing great code.&lt;/p&gt;&#10;&lt;h2 id="-4-ruff-the-fastest-python-linter-and-auto-formatter"&gt;🚀 4. Ruff: the fastest Python linter and auto-formatter &lt;a class="anchor" href="#-4-ruff-the-fastest-python-linter-and-auto-formatter" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In the past, I&amp;rsquo;ve used several tools to maintain code quality, such as isort, pyupgrade, flake8, and pydocstyle. However, I&amp;rsquo;ve recently discovered a powerful new tool called &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;Ruff&lt;/a&gt;), an extremely fast Python linter written in Rust. Ruff is not only capable of replacing all these tools but also executes much faster, which significantly improves the development workflow.&lt;/p&gt;&#10;&lt;p&gt;Ruff offers an impressive range of features and performance enhancements. Some of its standout features include:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;10-100x faster than existing linters&lt;/li&gt;&#10;&lt;li&gt;Over 500 built-in rules&lt;/li&gt;&#10;&lt;li&gt;Autofix support for automatic error correction&lt;/li&gt;&#10;&lt;li&gt;Built-in caching to avoid re-analyzing unchanged files&lt;/li&gt;&#10;&lt;li&gt;First-party editor integrations for VS Code and more&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Moreover, Ruff has gained widespread adoption in the Python community and is used in major open-source projects like Apache Airflow, FastAPI, Hugging Face, Pandas, and SciPy. The tool is backed by Astral, which provides active development and support.&lt;/p&gt;&#10;&lt;p&gt;One of the best aspects of Ruff is the native VS Code extension, which continuously scans your code and can fix problems as you write. This seamless integration makes it an indispensable part of any Python development workflow.&lt;/p&gt;&#10;&lt;p&gt;To install Ruff, simply run the following 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;pip install ruff&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To use Ruff in your project, run the following command in the terminal:&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;ruff your_file.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the best experience, I highly recommend using Ruff in conjunction with pre-commit, as mentioned earlier. This ensures that your code is always linted and formatted before committing, maintaining a consistent and high-quality codebase.&lt;/p&gt;&#10;&lt;p&gt;In conclusion, Ruff is an incredibly fast and powerful Python linter that streamlines the development process by consolidating multiple tools into a single, efficient package. By adopting Ruff and integrating it with pre-commit, you can enjoy a more productive and enjoyable Python development experience.&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;Pro tip: Go extreme, and use &lt;code&gt;--select ALL&lt;/code&gt; to enable all (&amp;gt;500) rules. This will ensure that your code is always in top shape.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="-5-mypy-static-type-checking-for-python-find-bugs-before-they-happen"&gt;🔍 5. Mypy: static type checking for Python (find bugs before they happen) &lt;a class="anchor" href="#-5-mypy-static-type-checking-for-python-find-bugs-before-they-happen" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;As developers, we&amp;rsquo;re always looking for ways to improve the quality of our code and minimize the time spent on debugging. This is where &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Mypy&lt;/a&gt; comes in. Mypy is a powerful static type checker for Python, designed to catch potential runtime errors before they occur. By using Mypy, you can ensure that your code adheres to type hints and that it runs smoothly without unexpected issues.&lt;/p&gt;&#10;&lt;p&gt;Mypy offers several advantages to Python developers, including:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Early detection of potential type errors, preventing many common bugs&lt;/li&gt;&#10;&lt;li&gt;Improved code readability and self-documentation through type annotations&lt;/li&gt;&#10;&lt;li&gt;Easier refactoring and code maintenance, as type annotations provide clear contracts between functions and modules&lt;/li&gt;&#10;&lt;li&gt;Better collaboration and communication within teams, since type annotations make the code&amp;rsquo;s intent and expectations more explicit&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Adding Mypy to an existing project may initially seem time-consuming and frustrating, as it might flag numerous issues that require attention. However, once you&amp;rsquo;ve addressed these concerns and properly set up Mypy, the benefits far outweigh the initial effort. In fact, the time you invest in fixing type-related issues will quickly be recovered as Mypy helps you prevent bugs and reduce debugging time.&lt;/p&gt;&#10;&lt;p&gt;For a long time, I too ignored Mypy and only typed my code without actually checking it with the tool. But after truly embracing Mypy and integrating it into my development process, I can confidently say that I will never go back. The value it brings to the table in terms of code quality, readability, and reliability is simply too great to ignore.&lt;/p&gt;&#10;&lt;p&gt;By proactively identifying and resolving type-related issues, Mypy not only saves you precious debugging time but also enhances the overall quality of your codebase. As you continue to rely on Mypy, you&amp;rsquo;ll find that it becomes an indispensable part of your Python development toolkit, ensuring that your projects remain robust and maintainable.&lt;/p&gt;&#10;&lt;p&gt;To start using Mypy in your project, first install it with the following 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;pip install mypy&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, you can run Mypy on your Python files by executing the following command in your terminal:&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;mypy your_file.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For even better integration, you can add Mypy as a pre-commit hook, ensuring that your code is always checked for type consistency before being committed. This further strengthens the code quality and helps prevent potential runtime errors.&lt;/p&gt;&#10;&lt;p&gt;In summary, incorporating Mypy into your Python development workflow adds an extra layer of safety and clarity, making it easier to write high-quality, reliable code. By combining Mypy with other tools like Ruff and pre-commit, you can create a robust development environment that promotes productivity and code quality.&lt;/p&gt;&#10;&lt;h2 id="-6-nox-and-pytest-for-testing-a-powerful-combination"&gt;🧪 6. Nox and pytest for testing: a powerful combination &lt;a class="anchor" href="#-6-nox-and-pytest-for-testing-a-powerful-combination" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When developing software, it&amp;rsquo;s essential to ensure that your code is reliable and functions as expected. The key to achieving this goal is writing thorough tests, which not only help you identify issues early on but also prevent regressions in the future. While it&amp;rsquo;s often tempting to dive headfirst into coding, taking a step back and first writing tests for your solution can lead to more efficient and effective development. Even if this approach doesn&amp;rsquo;t come naturally, it&amp;rsquo;s crucial to write tests for every problem you encounter to keep your codebase stable and reliable.&lt;/p&gt;&#10;&lt;p&gt;In the realm of Python testing, &lt;a href="https://nox.thea.codes/en/stable/" target="_blank" rel="noopener"&gt;Nox&lt;/a&gt; and &lt;a href="https://docs.pytest.org" target="_blank" rel="noopener"&gt;pytest&lt;/a&gt; stand out as powerful tools that can greatly enhance your testing process. Nox is a versatile automation tool that simplifies running tests in multiple virtual environments, while pytest is a popular and feature-rich testing framework for Python. Together, these tools provide a formidable testing environment that simplifies test management and execution.&lt;/p&gt;&#10;&lt;p&gt;In the past, you may have used &lt;a href="https://tox.wiki/en/latest/" target="_blank" rel="noopener"&gt;Tox&lt;/a&gt;, a tool that manages test environments with a text-based configuration file (e.g., in &lt;code&gt;setup.cfg&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt;). While Tox has served developers well for many years, including myself, Nox offers a more flexible alternative. Instead of relying on a text-based configuration, Nox takes advantage of a Python configuration file, providing virtually limitless configurability. This added flexibility makes it easy to create complex test matrices with different versions and dependencies, and even accommodate special conditions using simple if statements. After happily using Tox for years, I discovered Nox and found that it elevated my testing workflow to a whole new level.&lt;/p&gt;&#10;&lt;p&gt;To get started with Nox and pytest, create a basic &lt;code&gt;nox.py&lt;/code&gt; file and a sample test file for pytest. Here&amp;rsquo;s an example &lt;code&gt;nox.py&lt;/code&gt; configuration:&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;nox&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="nd"&gt;@nox.session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;python&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;3.7&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.9&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;tests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&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;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-r&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;requirements.txt&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;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-r&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;test-requirements.txt&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;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pytest&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;And an example test file for pytest:&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="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&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;test_addition&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;assert&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&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;test_subtraction&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;assert&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Beyond basic testing functionality, pytest boasts powerful features such as fixtures, mocking and patching, and parameterization. Pytest fixtures enable you to create reusable resources for your tests, while mocking and patching allow you to replace parts of your system with fake objects, isolating your tests from external dependencies. Parameterization, implemented using &lt;code&gt;pytest.mark.parametrize&lt;/code&gt;, enables running the same test with multiple sets of inputs and expected outputs.&lt;/p&gt;&#10;&lt;p&gt;In conclusion, leveraging Nox and pytest for testing offers numerous advantages for developers who strive to improve code reliability and automate their testing workflows. By harnessing Nox&amp;rsquo;s exceptional configurability and pytest&amp;rsquo;s extensive testing features, you&amp;rsquo;ll establish a robust testing environment that bolsters code quality and ensures optimal performance.&lt;/p&gt;&#10;&lt;h2 id="-7-versioningit-version-management-with-git-tags"&gt;🏷️ 7. Versioningit: version management with Git tags &lt;a class="anchor" href="#-7-versioningit-version-management-with-git-tags" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;(Optional, only related when making a Python package)&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;For developers seeking an automated and customizable package versioning solution, &lt;a href="https://versioningit.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Versioningit&lt;/a&gt; offers a powerful yet flexible approach. It is a setuptools plugin that automatically determines your package&amp;rsquo;s version based on your version control repository&amp;rsquo;s tags. The real advantage of Versioningit lies in its ability to customize the version format and even override separate functions used for version extraction and calculation.&lt;/p&gt;&#10;&lt;p&gt;Versioningit makes your release workflow extremely simple, especially if you have set up an automated build and twine PyPI upload pipeline. To create a new release, you only need to tag a version, and then click on the &amp;ldquo;Release&amp;rdquo; button in GitHub.&lt;/p&gt;&#10;&lt;p&gt;One of the major advantages of Versioningit is its minimal boilerplate code compared to other tools like &lt;a href="https://github.com/python-versioneer/python-versioneer" target="_blank" rel="noopener"&gt;Versioneer&lt;/a&gt;. Additionally, the Python &lt;code&gt;__version__&lt;/code&gt; string works seamlessly with editable installs (&lt;code&gt;pip install -e .&lt;/code&gt;), unlike &lt;a href="https://github.com/pypa/setuptools_scm" target="_blank" rel="noopener"&gt;setuptools_scm&lt;/a&gt;, which is a common issue faced by developers.&lt;/p&gt;&#10;&lt;p&gt;Over the years, I have explored many options for package versioning, including &lt;a href="https://github.com/python-versioneer/python-versioneer" target="_blank" rel="noopener"&gt;Versioneer&lt;/a&gt;, &lt;a href="https://github.com/jbweston/miniver" target="_blank" rel="noopener"&gt;Miniver&lt;/a&gt;, &lt;a href="https://github.com/pypa/setuptools_scm" target="_blank" rel="noopener"&gt;setuptools_scm&lt;/a&gt;, &lt;a href="https://github.com/peritus/bumpversion" target="_blank" rel="noopener"&gt;bumpversion&lt;/a&gt; (or &lt;a href="https://github.com/c4urself/bump2version" target="_blank" rel="noopener"&gt;bump2version&lt;/a&gt;), and manually setting everything. Versioningit stands out due to its simplicity, customization options, and compatibility with various workflows.&lt;/p&gt;&#10;&lt;p&gt;Some key features of Versioningit include:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Support for Git, modern Git archives, and Mercurial&lt;/li&gt;&#10;&lt;li&gt;Customizable version formatting using template strings&lt;/li&gt;&#10;&lt;li&gt;Optional writing of the final version and other details to a file for loading at runtime&lt;/li&gt;&#10;&lt;li&gt;Custom setuptools commands for inserting the final version and other details into a source file at build time&lt;/li&gt;&#10;&lt;li&gt;Customizable functions for VCS querying, tag-to-version calculation, version bumping, version formatting, and writing the version to a file&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;To use Versioningit in your project, simply add it to your project&amp;rsquo;s &lt;code&gt;pyproject.toml&lt;/code&gt; file in the &lt;code&gt;build-system.requires&lt;/code&gt; key. Then, create a &lt;code&gt;[tool.versioningit]&lt;/code&gt; table in your &lt;code&gt;pyproject.toml&lt;/code&gt; file. You can get up and running with just the minimal configuration - an empty table.&lt;/p&gt;&#10;&lt;p&gt;Once you have a &lt;code&gt;[tool.versioningit]&lt;/code&gt; table in your &lt;code&gt;pyproject.toml&lt;/code&gt; and your repository has at least one tag, building your project with &lt;code&gt;setuptools&lt;/code&gt; will automatically set your project&amp;rsquo;s version based on the latest tag in your Git repository. With Versioningit, you can customize the version format using placeholder strings, making it adaptable to your project&amp;rsquo;s specific needs.&lt;/p&gt;&#10;&lt;p&gt;In summary, Versioningit streamlines the package versioning process and simplifies the release workflow. By automating package versioning based on version control repository tags and providing extensive customization options, it helps make the management of package versions more efficient and less error-prone.&lt;/p&gt;&#10;&lt;h2 id="-8-pyprojecttoml-and-pep-621-configuration-consolidation"&gt;📝 8. Pyproject.toml and PEP 621: Configuration Consolidation &lt;a class="anchor" href="#-8-pyprojecttoml-and-pep-621-configuration-consolidation" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The &lt;code&gt;pyproject.toml&lt;/code&gt; file plays a crucial role in consolidating configurations and setup for various tools in a Python project, such as Black, Ruff, Mypy, Versioningit, Nox, and pytest. This adherence to &lt;a href="https://www.python.org/dev/peps/pep-0621/" target="_blank" rel="noopener"&gt;PEP 621&lt;/a&gt; standards streamlines the development process by providing a single source of truth for multiple tools, eliminating the need for additional configuration files like &lt;code&gt;setup.cfg&lt;/code&gt; or &lt;code&gt;setup.py&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;By specifying packaging details directly in the &lt;code&gt;pyproject.toml&lt;/code&gt; file, you can efficiently manage your project&amp;rsquo;s dependencies and metadata. For example, the Adaptive Scheduler package demonstrates this approach with a comprehensive &lt;code&gt;pyproject.toml&lt;/code&gt; file containing all the necessary configurations.&lt;/p&gt;&#10;&lt;details&gt;&#10;&lt;summary&gt;(click to unfold):&lt;/summary&gt;&#10;&lt;!-- CODE:START --&gt;&#10;&lt;!-- import urllib.request --&gt;&#10;&lt;!-- url = 'https://raw.githubusercontent.com/basnijholt/adaptive-scheduler/main/pyproject.toml' --&gt;&#10;&lt;!-- print("```toml") --&gt;&#10;&lt;!-- with urllib.request.urlopen(url) as response: --&gt;&#10;&lt;!-- content = response.read() --&gt;&#10;&lt;!-- print(content.decode('utf-8')) --&gt;&#10;&lt;!-- print("```") --&gt;&#10;&lt;!-- CODE:END --&gt;&#10;&lt;!-- OUTPUT:START --&gt;&#10;&lt;!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --&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;build-system&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;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;setuptools.build_meta&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="nx"&gt;requires&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;setuptools&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wheel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;versioningit&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler&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="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Run many `adaptive.Learner`s on many cores (&amp;gt;10k) using `mpi4py.futures`, `ipyparallel`, `dask-mpi`, or `process-pool`.&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="nx"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;gt;=3.8&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="nx"&gt;dynamic&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;version&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;maintainers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bas Nijholt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bas@nijho.lt&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;license&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;BSD-3&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;classifiers&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="s2"&gt;&amp;#34;Development Status :: 2 - Pre-Alpha&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="s2"&gt;&amp;#34;Intended Audience :: Developers&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="s2"&gt;&amp;#34;Intended Audience :: Science/Research&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="s2"&gt;&amp;#34;License :: OSI Approved :: BSD License&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="s2"&gt;&amp;#34;Operating System :: OS Independent&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="s2"&gt;&amp;#34;Programming Language :: Python&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="s2"&gt;&amp;#34;Programming Language :: Python :: 3&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="s2"&gt;&amp;#34;Programming Language :: Python :: 3.8&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="s2"&gt;&amp;#34;Programming Language :: Python :: 3.9&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="s2"&gt;&amp;#34;Programming Language :: Python :: 3.10&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="s2"&gt;&amp;#34;Programming Language :: Python :: 3.11&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="s2"&gt;&amp;#34;Topic :: Scientific/Engineering&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="s2"&gt;&amp;#34;Topic :: System :: Distributed Computing&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="nx"&gt;dependencies&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="s2"&gt;&amp;#34;adaptive &amp;gt;= 0.14.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="s2"&gt;&amp;#34;cloudpickle&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="s2"&gt;&amp;#34;dill&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="s2"&gt;&amp;#34;ipyparallel&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="s2"&gt;&amp;#34;ipywidgets&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="s2"&gt;&amp;#34;itables&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="s2"&gt;&amp;#34;jinja2&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="s2"&gt;&amp;#34;loky&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="s2"&gt;&amp;#34;numpy&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="s2"&gt;&amp;#34;pandas&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="s2"&gt;&amp;#34;psutil&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="s2"&gt;&amp;#34;pyarrow&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="s2"&gt;&amp;#34;pyzmq&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="s2"&gt;&amp;#34;rich&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="s2"&gt;&amp;#34;structlog&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="s2"&gt;&amp;#34;tinydb&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="s2"&gt;&amp;#34;toolz&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="s2"&gt;&amp;#34;tqdm&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="s2"&gt;&amp;#34;versioningit&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;&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;all&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;dask-mpi&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mpi4py&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;test&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;pytest&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytest-asyncio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;coverage&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytest-cov&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;docs&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="s2"&gt;&amp;#34;myst-nb&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="s2"&gt;&amp;#34;sphinx_fontawesome&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="s2"&gt;&amp;#34;sphinx&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="s2"&gt;&amp;#34;furo&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="s2"&gt;&amp;#34;myst-parser&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="s2"&gt;&amp;#34;emoji&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;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&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;homepage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://adaptive-scheduler.readthedocs.io/&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="nx"&gt;documentation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://adaptive-scheduler.readthedocs.io/&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="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://github.com/basnijholt/adaptive-scheduler&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="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;readme&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;content-type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text/x-rst&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="nx"&gt;file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;README.rst&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="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;scripts&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;adaptive-scheduler-launcher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler._server_support.launcher:main&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versioningit&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versioningit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onbuild&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;build-file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler/_version.py&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="nx"&gt;source-file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler/_version.py&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setuptools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&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;include&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;adaptive_scheduler.*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ini_options&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;addopts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&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; -vvv&#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; --cov=adaptive_scheduler&#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; --cov-report term&#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; --cov-report html&#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; --cov-report xml&#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; --cov-fail-under=35&#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; --asyncio-mode=auto&#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;&amp;#34;&amp;#34;&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;report&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;exclude_lines&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="s2"&gt;&amp;#34;pragma: no cover&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="s2"&gt;&amp;#34;raise NotImplementedError&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="s2"&gt;&amp;#34;if TYPE_CHECKING:&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="s2"&gt;&amp;#34;if __name__ == .__main__.:&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;&#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;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;black&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;line_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;88&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruff&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;line-length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&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;target-version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;py38&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="nx"&gt;select&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;ALL&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;ignore&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="s2"&gt;&amp;#34;T20&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# flake8-print&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;ANN101&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Missing type annotation for {name} in method&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;S101&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Use of assert detected&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;PD901&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# df is a bad variable name. Be kinder to your future self.&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;ANN401&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Dynamically typed expressions (typing.Any) are disallowed in {name}&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;D402&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# First line should not be the function&amp;#39;s signature&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;PLW0603&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Using the global statement to update `X` is discouraged&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;D401&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# First line of docstring should be in imperative mood&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;SLF001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Private member accessed&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;&#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;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;per-file-ignores&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;tests/*&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;SLF001&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="s2"&gt;&amp;#34;tests/test_examples.py&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;E501&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="s2"&gt;&amp;#34;.github/*&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;INP001&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mccabe&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;max-complexity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mypy&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;python_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.8&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;!-- OUTPUT:END --&gt;&#10;&lt;/details&gt;&#10;&lt;p&gt;(from &lt;a href="https://github.com/basnijholt/adaptive-scheduler/blob/main/pyproject.toml" target="_blank" rel="noopener"&gt;here&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;As seen in the example, the &lt;code&gt;pyproject.toml&lt;/code&gt; file includes details about the build system, project metadata, dependencies, optional dependencies, URLs, and configurations for various tools such as Versioningit, setuptools, pytest, coverage, Black, Ruff, and Mypy. By consolidating these configurations, you can maintain a more organized and maintainable project, making it easier to understand and update settings for different tools.&lt;/p&gt;&#10;&lt;p&gt;In summary, the &lt;code&gt;pyproject.toml&lt;/code&gt; file and adherence to PEP 621 provide a unified approach to configuring and setting up your Python project, making it easier to manage dependencies, metadata, and tool-specific settings. This streamlined process ultimately leads to a more efficient and maintainable development workflow.&lt;/p&gt;&#10;&lt;h2 id="-9-github-actions-ci-matrix-testing-on-multiple-platforms-and-auto-publishing-to-pypi"&gt;🌐 9. GitHub Actions CI Matrix: Testing on Multiple Platforms and Auto Publishing to PyPI &lt;a class="anchor" href="#-9-github-actions-ci-matrix-testing-on-multiple-platforms-and-auto-publishing-to-pypi" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener"&gt;GitHub Actions&lt;/a&gt; is an excellent tool for automating workflows and improving code quality through continuous integration (CI). By using a CI matrix, you can test your code on multiple platforms and Python versions, ensuring that your package remains compatible and stable across different environments.&lt;/p&gt;&#10;&lt;p&gt;To set up a &lt;code&gt;.github/workflows/nox.yaml&lt;/code&gt; file for GitHub Actions, you can use the following simplified example:&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;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;nox&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;on&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;pull_request&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;push&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;branches&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;main]&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;jobs&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;test&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;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ matrix.platform }}&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;strategy&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;fail-fast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;matrix&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;platform&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;ubuntu-latest, macos-latest, windows-latest]&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;python-version&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="s2"&gt;&amp;#34;3.8&amp;#34;&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;3.9&amp;#34;&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;3.10&amp;#34;&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;3.11&amp;#34;&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;&#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;steps&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;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v2&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;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;Set up Python ${{ matrix.python-version }}&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;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-python@v2&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;with&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;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ matrix.python-version }}&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;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;Install dependencies&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;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pip install nox&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;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;Test with nox&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;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nox -e py${{ matrix.python-version }}&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;This configuration file specifies the platforms and Python versions for testing, as well as the steps to set up the environment, install dependencies, and run tests using nox.&#10;With this simple configuration, you set up tests on MacOS, Windows, Linux, and multiple Python versions, ensuring that your package remains compatible across different environments.&lt;/p&gt;&#10;&lt;p&gt;Another essential part of the CI/CD process is deploying your Python package. You can use the &lt;code&gt;.github/workflows/publish-package.yaml&lt;/code&gt; file to automate the package deployment to PyPI whenever a new release is published:&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;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;Upload Python Package&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;on&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;release&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;types&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;published]&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;jobs&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;deploy&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;&#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;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&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;steps&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;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v2&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;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;Set up Python&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;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-python@v2&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;with&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;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;3.x&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;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;Install dependencies&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;run&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="sd"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; python -m pip install --upgrade pip&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; pip install setuptools wheel twine build&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;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;Build and publish&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;env&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;TWINE_USERNAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;__token__&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;TWINE_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.PYPI_TOKEN }}&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;run&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="sd"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; python -m build&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; twine upload dist/*&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;This workflow sets up Python, installs necessary dependencies, builds the package, and publishes it to PyPI using Twine. The process integrates seamlessly with versioningit, ensuring that the published package has the correct version number based on the git history.&lt;/p&gt;&#10;&lt;p&gt;In summary, leveraging GitHub Actions for continuous integration and automated workflows enhances code quality and reliability, while also simplifying the development and deployment processes. By testing across various platforms and Python versions, and automating package deployment, you can be confident that your package is well-maintained and accessible to users.&lt;/p&gt;&#10;&lt;h2 id="-10-read-the-docs-sphinx-myst-nb-and-jupytext-notebooks-for-documentation"&gt;📚 10. Read the Docs, Sphinx, MyST-NB, and Jupytext notebooks for documentation &lt;a class="anchor" href="#-10-read-the-docs-sphinx-myst-nb-and-jupytext-notebooks-for-documentation" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Documentation is crucial for any software project, as it helps users understand how to use the package and provides guidance for contributors. &lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;Read the Docs&lt;/a&gt;, &lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;Sphinx&lt;/a&gt;, &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt;, and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext notebooks&lt;/a&gt; are powerful tools that, when combined, facilitate the creation and maintenance of high-quality documentation.&lt;/p&gt;&#10;&lt;p&gt;tl;dr: check out the &lt;a href="https://adaptive.readthedocs.io/" target="_blank" rel="noopener"&gt;Adaptive documentation&lt;/a&gt; for an example of how to use these tools to create a comprehensive documentation website.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Read the Docs&lt;/strong&gt;&lt;/a&gt; is a documentation hosting platform that automatically builds, version controls, and hosts your documentation for free. It integrates well with Sphinx and GitHub, allowing your documentation to be updated whenever you push changes to your repository.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Sphinx&lt;/strong&gt;&lt;/a&gt; is a documentation generator that can transform reStructuredText files, MyST files, or Jupyter notebooks into various output formats, such as HTML, LaTeX, or PDF. It is highly extensible, supporting custom themes and plugins, and can automatically generate API documentation from your Python code.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;MyST-NB&lt;/strong&gt;&lt;/a&gt; is an extension to Sphinx that enables it to parse Jupyter notebooks and execute the code cells during the build process. It uses the MyST (short for Markedly Structured Text) markdown parser, which adds support for Sphinx roles and directives, making it a powerful alternative to reStructuredText.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Jupytext notebooks&lt;/strong&gt;&lt;/a&gt; is a Jupyter extension that enables you to save Jupyter notebooks in Markdown format (or other text-based formats) instead of the default JSON format. By using Jupytext notebooks, you can store your code, explanations, and visualizations as plain text, which is easier to version control and can be seamlessly integrated with MyST-NB and Sphinx.&lt;/p&gt;&#10;&lt;p&gt;With &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt; and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext&lt;/a&gt;, you can write your documentation using Jupyter notebooks in Markdown format. This approach combines the benefits of Jupyter notebooks, such as interactive code execution and rich output, with the simplicity of Markdown and the powerful features of Sphinx. The result is a highly readable, user-friendly, and maintainable documentation that caters to both users and developers.&lt;/p&gt;&#10;&lt;p&gt;To get started with MyST-NB, you&amp;rsquo;ll need to install it using pip:&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 myst-nb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, configure your Sphinx project to use MyST-NB by adding the following lines to your &lt;code&gt;conf.py&lt;/code&gt; file:&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;extensions&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;myst_nb&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;Now, Sphinx will be able to parse and execute your Jupyter notebooks during the documentation build process, integrating them seamlessly with your existing documentation.&lt;/p&gt;&#10;&lt;p&gt;In summary, using Read the Docs, Sphinx, MyST-NB, and Jupytext notebooks for your documentation enables you to create high-quality, interactive, and easily maintainable documentation that benefits your project&amp;rsquo;s users and contributors. This combination of tools ensures that your documentation remains up-to-date, easily navigable, and engaging, ultimately enhancing the overall experience for your project&amp;rsquo;s audience.&lt;/p&gt;&#10;&lt;p&gt;For a full example, see how I setup &lt;a href="https://adaptive.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Adaptive&amp;rsquo;s documentation&lt;/a&gt; using Sphinx, MyST-NB, and Jupytext notebooks. &lt;a href="https://github.com/python-adaptive/adaptive/blob/1b0f15e44235643731c56d335f6711c5584d4828/readthedocs.yml" target="_blank" rel="noopener"&gt;&lt;code&gt;readthedocs.yaml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/python-adaptive/adaptive/blob/1b0f15e44235643731c56d335f6711c5584d4828/docs/source/conf.py" target="_blank" rel="noopener"&gt;&lt;code&gt;docs/source/conf.py&lt;/code&gt;&lt;/a&gt;, and here is an example notebook that is used in the &lt;a href="https://adaptive.readthedocs.io/en/latest/tutorial/tutorial.Learner1D.html" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt;: &lt;a href="https://github.com/python-adaptive/adaptive/blob/main/docs/source/tutorial/tutorial.Learner1D.md" target="_blank" rel="noopener"&gt;&lt;code&gt;docs/source/tutorial/tutorial.Learner1D.md&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="-11-conclusion"&gt;🎉 11. Conclusion &lt;a class="anchor" href="#-11-conclusion" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In conclusion, incorporating these powerful tools into your Python development workflow can significantly enhance your productivity and code quality. By adopting &lt;a href="https://pre-commit.com/" target="_blank" rel="noopener"&gt;pre-commit&lt;/a&gt;, &lt;a href="https://black.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Black&lt;/a&gt;, &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;Ruff&lt;/a&gt;, &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Mypy&lt;/a&gt;, &lt;a href="https://versioningit.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Versioningit&lt;/a&gt;, &lt;a href="https://www.python.org/dev/peps/pep-0621/" target="_blank" rel="noopener"&gt;Pyproject.toml with PEP 621&lt;/a&gt;, &lt;a href="https://nox.thea.codes/en/stable/" target="_blank" rel="noopener"&gt;Nox&lt;/a&gt;, &lt;a href="https://docs.pytest.org/" target="_blank" rel="noopener"&gt;pytest&lt;/a&gt;, &lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener"&gt;GitHub Actions CI matrix&lt;/a&gt;, and &lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;Read the Docs&lt;/a&gt;, &lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;Sphinx&lt;/a&gt;, &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt;, and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext notebooks&lt;/a&gt; for documentation, you create a streamlined, efficient, and reliable development environment.&lt;/p&gt;&#10;&lt;p&gt;These tools help maintain code consistency, catch issues early, automate package versioning, simplify testing, consolidate configurations, and create high-quality documentation. By integrating them into your projects, you can focus on writing great code while ensuring that your work remains robust, maintainable, and accessible to users and contributors alike.&lt;/p&gt;&#10;&lt;p&gt;Don&amp;rsquo;t hesitate to explore these tools and incorporate them into your Python development toolkit. They can transform your development experience, making it more enjoyable and efficient, ultimately leading to higher-quality projects and happier developers. Happy coding!&lt;/p&gt;&#10;</description></item><item><title>Crafting unique Ph.D. thesis covers with Python 🎨</title><link>https://www.nijho.lt/post/thesis-cover/</link><pubDate>Sat, 04 Apr 2020 13:40:00 +0000</pubDate><guid isPermaLink="false">/post/thesis-cover/</guid><description>&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://quantumtinkerer.tudelft.nl/blog/thesis-cover/" target="_blank" rel="noopener"&gt;quantumtinkerer.tudelft.nl/blog/thesis-cover&lt;/a&gt; on 2020-04-04 13:40.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;I will hand out around a hundred copies of my thesis to my defense committee, colleagues, and my family and friends.&#10;Let&amp;rsquo;s be honest, most people will probably not get further than attempting to read the summary and appreciating the cover.&#10;Four years of work has gone into generating the content of the thesis, so I figured, at least some thought has to go into the design of its cover.&#10;Unfortunately, I am by no means an expert on a graphical design or even competent enough to attempt to use any kind of graphic design software.&#10;But luckily for me, I do consider myself an expert in Python &lt;i class="fab fa-python"&gt;&lt;/i&gt;, and why not make the thesis design a fun process?&lt;/p&gt;&#10;&lt;h2 id="initial-design-adaptive-sampling-and-data-visualization"&gt;Initial Design: Adaptive Sampling and Data Visualization &lt;a class="anchor" href="#initial-design-adaptive-sampling-and-data-visualization" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My vague plan was to somehow incorporate a plot resulting from a simulation run with &lt;a href="https://adaptive.readthedocs.io/" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt; [&lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;source&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;], the software I wrote during my Ph.D. to adaptively sample functions in parallel and my proudest work.&#10;I even had a potential simulation result in mind that I would use for it, specifically, one that has been on the background of my good friend’s phone for some years and one that has earned me quite some internet points on &lt;a href="https://www.reddit.com/r/dataisbeautiful/comments/eaap3p/python_quantum_research_art_saving_1000s_hours_of/" target="_blank" rel="noopener"&gt;Reddit&lt;/a&gt;.&#10;This plot (below) is one of the results of Chapter 4 of &lt;a href="http://files.nijho.lt/thesis.pdf" target="_blank" rel="noopener"&gt;my thesis&lt;/a&gt; [&lt;a href="https://github.com/basnijholt/thesis" target="_blank" rel="noopener"&gt;source&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;] and &lt;a href="https://arxiv.org/abs/1807.01940%ef%bf%bc" target="_blank" rel="noopener"&gt;this paper&lt;/a&gt;.&lt;/p&gt;&#10;&lt;video style="width: 100%;max-width: 500px;" controls&gt;&#10; &lt;source src="https://user-images.githubusercontent.com/6897215/233811442-ce9bc475-af15-4b89-9578-67925419b769.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10;&lt;/video&gt;&#10;&lt;p&gt;I went back to the code that generates the plot and started hacking away in Matplotlib.&#10;Quickly, I realized that not only the plot above was going to look cool, but that many other data sets that I had also resulted in beautiful patterns.&#10;I hit the paradox of choice: I had a total of &lt;em&gt;3671&lt;/em&gt; unique data sets (all for Majorana devices with either different parameter sweeps or different physical effects that were included in the model) and no idea how I was going to make a selection.&lt;/p&gt;&#10;&lt;p&gt;This is an early attempt (and rather ugly in hindsight)&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811538-dc3b5b86-6f6f-4bd6-840c-e466918ce151.jpeg" alt="early-attempt" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;I decided to postpone the choice of which data to use and just focus on how to plot the data first.&#10;I experimented with different colormaps, fading colormaps that gradually reveal the underlying triangulation better, and more.&#10;On a weekend night, after telling &lt;a href="https://github.com/sbalk" target="_blank" rel="noopener"&gt;a friend&lt;/a&gt; I was with about my plans, we (as usually happens) started to program some of the ideas we came up with.&#10;We settled on a design where the triangulation would slowly fade from the top to bottom and where each triangle simultaneously transitioned from a solid color (the average of the values at its vertices) to densely interpolated values.&#10;See &lt;a href="https://github.com/basnijholt/thesis-cover/blob/master/generate-covers.ipynb" target="_blank" rel="noopener"&gt;this Jupyter notebook&lt;/a&gt; which generated the final version of the cover.&#10;After we also chose a nice colormap (by simply creating the cover for each possible colormap,) the following was suggested: “You know what would be even cooler?! To make every cover unique.”&#10;Ten minutes later, I had started 3671 jobs on our computational cluster.&#10;When the files slowly started to appear on my computer (I have my local files synced with the cluster), we could not have been more excited.&#10;There were simply so many cool looking alternatives.&#10;Too many to manually select now, to be honest&amp;mdash;a problem for later I thought.&lt;/p&gt;&#10;&lt;h2 id="overcoming-the-paradox-of-choice-clustering-and-neural-networks"&gt;Overcoming the Paradox of Choice: Clustering and Neural Networks &lt;a class="anchor" href="#overcoming-the-paradox-of-choice-clustering-and-neural-networks" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Four months later, with the rest of my thesis completely done and about a day left on the deadline to submit my thesis, I found myself facing the selection problem I had successfully ignored for so long.&#10;There must be a smarter way to select the covers, I thought.&#10;Many of them look roughly similar (and some of them are even quite boring or not ecstatically pleasing), so it does not make sense to scroll through all of them.&#10;I could cluster them by similarity and then I would just have to choose the nicely looking clusters while being able to instantly discard many.&lt;/p&gt;&#10;&lt;p&gt;Finally, a good application to use neural networks!&lt;/p&gt;&#10;&lt;p&gt;I still had the 3671 covers in pdf format in some folder, so using the wonderful &lt;a href="https://www.gnu.org/software/parallel/" target="_blank" rel="noopener"&gt;GNU Parallel&lt;/a&gt;, I converted and rasterized all of them to jpeg with&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;find . -iname &lt;span class="se"&gt;\*&lt;/span&gt;pdf -print0 &lt;span class="p"&gt;|&lt;/span&gt; parallel -0 sips -s format jpeg &lt;span class="o"&gt;{}&lt;/span&gt; --out &lt;span class="o"&gt;{&lt;/span&gt;.&lt;span class="o"&gt;}&lt;/span&gt;.jpg&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I used a pre-trained convolutional neural network model (&lt;a href="https://keras.io/applications/#vgg16" target="_blank" rel="noopener"&gt;VGG16&lt;/a&gt;) to extract features in a lower-dimensional representation.&#10;I fed the feature arrays to a &lt;a href="https://en.wikipedia.org/wiki/K-means_clustering" target="_blank" rel="noopener"&gt;K-Means clustering algorithm&lt;/a&gt; that sorted them in 80 clusters.&#10;To procrastinate a little more, I created an interactive widget using &lt;a href="https://ipywidgets.readthedocs.io/" target="_blank" rel="noopener"&gt;&lt;code&gt;ipywidgets&lt;/code&gt;&lt;/a&gt; (see video below) to facilitate the selection process.&lt;/p&gt;&#10;&lt;video style="width: 100%;max-width: 800px;" controls&gt;&#10; &lt;source src="https://user-images.githubusercontent.com/6897215/233811575-98ea0be2-33f4-47f4-b3ee-f8e7bdf3c475.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10;&lt;/video&gt;&#10;&lt;p&gt;These are the clusters that I selected in a grid&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811546-36be2cec-eb8c-437a-88d1-595a8602a7ba.jpeg" alt="chosen-clusters-front-and-back" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;The selection process was done using &lt;a href="https://github.com/basnijholt/thesis-cover/blob/master/thesis-cover-selection.ipynb" target="_blank" rel="noopener"&gt;this Jupyter notebook&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Finally, I copied the selected clusters into different folders and used the OS&amp;rsquo;s file explorer to manually select a few covers from each cluster.&lt;/p&gt;&#10;&lt;h2 id="final-touches-fine-tuning-colors-and-adding-edition-numbers"&gt;Final Touches: Fine-Tuning Colors and Adding Edition Numbers &lt;a class="anchor" href="#final-touches-fine-tuning-colors-and-adding-edition-numbers" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Then I printed an initial version of my thesis, however, I wasn&amp;rsquo;t really happy with the colors.&lt;/p&gt;&#10;&lt;p&gt;To choose the right color scheme I got help from a non-tech savvy friend who went to art school.&#10;Due to the COVID-19 crisis, we couldn&amp;rsquo;t sit side by side, so I created &lt;a href="https://mybinder.org/v2/gh/basnijholt/thesis-cover/master?filepath=color-picker.ipynb" target="_blank" rel="noopener"&gt;this Binder &amp;ldquo;app&amp;rdquo;&lt;/a&gt; with sliders.&lt;/p&gt;&#10;&lt;video style="width: 100%;max-width: 800px;" controls&gt;&#10; &lt;source src="https://user-images.githubusercontent.com/6897215/233811609-8fa74955-0c51-443e-9a87-115921530b7d.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10;&lt;/video&gt;&#10;&lt;p&gt;Additionally, since all of them were different anyway, I gave them an identifier and added&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;edition n of 120&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;to the back.&lt;/p&gt;&#10;&lt;p&gt;These are a few examples of the final result (click on one of the images to open gallery view)&lt;/p&gt;&#10;&lt;div class="gallery"&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0061.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0061.pickle_hu_8e2f6288e2299f4b.webp" width="350" height="242" alt="Image 1 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0087.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0087.pickle_hu_cfe0a7647b8b4d1a.webp" width="350" height="242" alt="Image 2 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0094.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0094.pickle_hu_4767f3839b1f22bc.webp" width="350" height="242" alt="Image 3 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0119.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0119.pickle_hu_9a20ab2d5318b96f.webp" width="350" height="242" alt="Image 4 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0254.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0254.pickle_hu_e670a31aca40cc2e.webp" width="350" height="242" alt="Image 5 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha2__data_learner_0175.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha2__data_learner_0175.pickle_hu_236481dab96c15b5.webp" width="350" height="242" alt="Image 6 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-so-slowly__data_learner_0457.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-SO-slowly__data_learner_0457.pickle_hu_cc81d6c6a833f7ce.webp" width="350" height="242" alt="Image 7 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-so-slowly__data_learner_0699.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-SO-slowly__data_learner_0699.pickle_hu_4744ce916ab208d2.webp" width="350" height="242" alt="Image 8 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-so-slowly__data_learner_0811.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-SO-slowly__data_learner_0811.pickle_hu_5f6e91d59b7c978e.webp" width="350" height="242" alt="Image 9 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0179.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0179.pickle_hu_ad9a40d0f92c3c0f.webp" width="350" height="242" alt="Image 10 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0254.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0254.pickle_hu_2e0e64805618b36d.webp" width="350" height="242" alt="Image 11 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0255.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0255.pickle_hu_e7bc31a50788a8cd.webp" width="350" height="242" alt="Image 12 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;p&gt;A few days later, this was delivered to my doorstep&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811518-bd5426d6-05bc-4c7a-854a-4c373aa21fcf.jpg" alt="theses-everywhere" loading="lazy" data-zoomable&gt;&#10;&lt;img src="https://user-images.githubusercontent.com/6897215/233811525-645b67a4-a9ea-48ba-a391-4639ed92c19d.jpg" alt="printed" loading="lazy" data-zoomable&gt;&lt;/p&gt;&#10;&lt;p&gt;And finally I mailed them to my Ph.D. committee&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811533-3b85ff8d-8359-453b-8aa1-ef653629a0f4.jpg" alt="mail" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;Now it&amp;rsquo;s time to start preparing for my defense at May 11, 2020 at 15:00!&lt;/p&gt;&#10;&lt;h3 id="notes"&gt;Notes &lt;a class="anchor" href="#notes" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The data and code to generate all printed covers is available at &lt;a href="https://github.com/basnijholt/thesis-cover" target="_blank" rel="noopener"&gt;basnijholt/thesis-cover&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;&lt;/li&gt;&#10;&lt;li&gt;The simulation code that generates the data is available at &lt;a href="https://github.com/basnijholt/spin-orbit-nanowires" target="_blank" rel="noopener"&gt;basnijholt/spin-orbit-nanowires&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;&lt;/li&gt;&#10;&lt;li&gt;The video of the Reddit post is generated using &lt;a href="https://gist.github.com/basnijholt/7aaeb7b9c506f830e6791d264a6cf467" target="_blank" rel="noopener"&gt;this code&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item></channel></rss>