&lt;?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>build.golfnext.com</title>
    <link>https://build.golfnext.com/en/</link>
    <description>Engineering, operations, and infrastructure at a small, ambitious team.</description>
    <language>en-US</language>
    <copyright>© 2026 GolfNext ApS · Ry, Denmark</copyright><lastBuildDate>Thu, 27 Aug 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://build.golfnext.com/en/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Delete the setup guide</title>
      <link>https://build.golfnext.com/en/posts/delete-the-setup-guide/</link>
      <pubDate>Thu, 27 Aug 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/delete-the-setup-guide/</guid>
      <description>Every repository ships instructions nobody keeps up to date, and a .env.example somebody has to fill in from a Slack message. Two tools — mise for the toolchain, fnox for the secrets — take both jobs out of the README and put them in the repo, with a fair account of what that costs and what it doesn&#39;t fix.</description>
      <content:encoded><![CDATA[<p>Think about the last time you cloned a repository you&rsquo;d never worked in before.</p>
<p>You read the README. It told you to install a JDK, and you had the wrong one, so you went and found the right one. It told you to install Node, and the version file said 18 while the lockfile clearly wanted 20. Somewhere near the bottom there was a line saying <code>cp .env.example .env</code> and then, in a smaller font, <em>fill in the values</em>. Fill them in from where? From a colleague. So you asked in Slack, someone pasted you a block of <code>KEY=value</code> lines, and now a production-adjacent credential lives in a chat history forever: searchable, backed up, exported to whatever compliance tool the company bought last year.</p>
<p>Nobody set out to build that. It&rsquo;s what happens when the instructions live in a document instead of in the repository.</p>
<p>I&rsquo;m the tech lead at GolfNext, a Danish company that builds software for golf clubs, and my first post on this blog was an inventory of what I&rsquo;d walked into. Sixty-eight repositories, four product lines, thirteen years of accumulated technology choices: Java and Maven and Spring, Kotlin and Gradle, Node, Python, PHP, JavaFX, React Native. Starting work in one of them cold took somewhere between two and six hours depending which one you picked, and I set myself a target of five minutes from clone to running code.</p>
<p>This is the first serious attempt at that. Two tools, both by the same person, jdx. <a href="https://mise.jdx.dev/">mise</a> puts the right tools on your PATH, <a href="https://fnox.jdx.dev/">fnox</a> puts the right secrets in your environment, and those are the two halves of one problem. mise is boring and widely used. fnox is neither of those things yet, and I&rsquo;ll come back to that. What&rsquo;s interesting is what falls away once both are in place, and what it does to the coding agents I have running in these repositories all day.</p>
<p>It&rsquo;s <em>mise</em> as in <em>mise en place</em>, the kitchen thing. Pronounced &ldquo;meez&rdquo;, /miːz/, one syllable, no <em>-uh</em> on the end. You want that settled before you say it out loud in a meeting. fnox is easier: Fort Knox with most of the letters knocked out, so &ldquo;eff-nox&rdquo;, and the name tells you what it&rsquo;s for.</p>
<h2 id="half-one-the-tools">Half one: the tools<a class="gn-heading-anchor" href="#half-one-the-tools" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>If you&rsquo;ve used nvm or pyenv you know the shape: declare a version, <code>cd</code> in, get that version. The difference is that mise doesn&rsquo;t care what the version is <em>of</em>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># .mise.toml</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">java</span> <span class="p">=</span> <span class="s2">&#34;21&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">maven</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">node</span> <span class="p">=</span> <span class="s2">&#34;lts&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">python</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">gitleaks</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span></code></pre></div><p>That&rsquo;s a JVM, a build tool, a JavaScript runtime, a Python and a secret scanner (<a href="https://github.com/gitleaks/gitleaks">gitleaks</a>) in one file, installed by one command, on your PATH only inside this directory tree. Add <code>mise activate</code> to your shell profile and even the command goes away: <code>cd</code> into the repo and the tools are there, <code>cd</code> out and they aren&rsquo;t. It also sets the environment variables that go with them, so <code>JAVA_HOME</code> points at the JDK this project pinned rather than whatever your operating system decided last.</p>
<p>&ldquo;Version manager&rdquo; undersells this, and I want to be specific about why. The expensive part of a polyglot estate is the switching, not any single stack. You spend a morning in a Spring service, then move to an Android repo, then a small Python tool, and each hop is a chance for your shell to be wrong in a way that produces a confusing error rather than an obvious one. In one of our repositories a newer system JDK doesn&rsquo;t fail loudly. It breaks a mocking library during tests, several layers down, with a message that has nothing to do with Java versions. That&rsquo;s an hour of somebody&rsquo;s life, and you pay it again every time a new person joins or a laptop gets replaced.</p>
<p>A per-language tool can&rsquo;t help, because the problem is <em>between</em> languages.</p>
<p>One nuance you may as well steal instead of discovering. We deliberately let some of those versions float: <code>latest</code> for the scanner, <code>lts</code> for Node. What a <em>build</em> is made of is pinned in the CI workflow, and that pin is the one dependency automation watches and raises pull requests against. If we pinned the same versions in <code>.mise.toml</code> as well, we&rsquo;d have two places to keep in step, and one of them would quietly fall behind. We know this because it had already happened, a <code>.mise.toml</code> claiming Node 22 while all four workflows said 24. Only one file gets to be authoritative, and it&rsquo;s the workflow.</p>
<p>The JDK is the exception, pinned hard in both places on purpose. A wrong major version there is exactly the mocking-library failure above, and I&rsquo;d rather keep two files in step than rediscover that error at four in the afternoon. Everything else floats.</p>
<h2 id="the-part-i-didnt-expect-to-care-about">The part I didn&rsquo;t expect to care about<a class="gn-heading-anchor" href="#the-part-i-didnt-expect-to-care-about" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Every repository has a handful of things you do to it: build it, test it, run it, format it. In a Maven project that&rsquo;s <code>mvn spring-boot:run -Dspring-boot.run.profiles=local</code>. In a Gradle project it&rsquo;s <code>./gradlew</code> and a task name with four words glued together. In a Node project it&rsquo;s whatever the <code>scripts</code> block says this week. None of it is hard. All of it is something you have to look up when you&rsquo;re in an unfamiliar repo, which is exactly when you have the least patience for looking things up.</p>
<p>mise lets you put those verbs in the same file as the tools:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">build</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Package the artifact&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn clean package -B&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">test</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Full test suite (integration tests need Docker)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn clean test -B&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">serve</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run the API on http://127.0.0.1:5001 (starts the stack first)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">depends</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;stack:up&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn spring-boot:run -Dspring-boot.run.profiles=local&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="s2">&#34;stack:up&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Start the local database and the mocks. Idempotent&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;docker compose up -d --wait&#34;</span>
</span></span></code></pre></div><p>So the interface becomes <code>mise run build</code>, <code>mise run test</code>, <code>mise run serve</code>. In the Android repository next door the same verbs sit on top of Gradle instead, and one of them wraps a four-word task name I still can&rsquo;t type from memory. Doesn&rsquo;t matter. <code>mise tasks</code> lists what a repository can do, with descriptions, and the answer is in the repo rather than in a wiki page that stopped being true in 2023.</p>
<p>Two small conventions have earned their place. A bare verb is the common case and <code>verb:qualifier</code> narrows it: <code>test</code> and <code>test:signer</code>, <code>stack:up</code> and <code>stack:down</code> and <code>stack:clean</code>. There&rsquo;s also always an escape hatch task that wraps the underlying build tool directly, so anything the curated list doesn&rsquo;t cover is still one command away:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">mvn</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Any Maven command, with this repo&#39;s JDK and environment&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;mvn&#34;</span>
</span></span></code></pre></div><p>That escape hatch matters for the second half of the story.</p>
<h2 id="half-two-the-secrets">Half two: the secrets<a class="gn-heading-anchor" href="#half-two-the-secrets" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>This is the part I actually wanted to fix.</p>
<p>The <code>.env.example</code> pattern is so common that it reads as a best practice. It&rsquo;s a convention that gave up. Look at what it asks of a team.</p>
<p>A new developer copies a file of empty keys and then has to source real values from somewhere the repository can&rsquo;t tell them about. In practice that means a person, which means a chat message, and now a secret exists in a second system with different retention rules and different access control than the one you chose on purpose. When a value rotates, every laptop holding the old one is silently wrong until someone notices, and the failure usually shows up as a confusing 401 rather than a message saying &ldquo;your credential is four months stale&rdquo;. When a new secret is added, everyone else&rsquo;s <code>.env</code> is incomplete and nobody is told. And the file itself sits in your working directory in plaintext, one bad <code>git add -A</code> away from being committed, which is why every repository that uses this pattern also has a <code>.gitignore</code> line that somebody could delete.</p>
<p>The alternative isn&rsquo;t complicated. Put the values in a secret store you already trust, put <em>references</em> to them in the repository, and let each developer&rsquo;s own identity decide whether they resolve.</p>
<p>That&rsquo;s fnox. Here&rsquo;s the stage config for a service, committed, with no values anywhere in it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="c"># fnox.stage.toml</span>
</span></span><span class="line"><span class="cl"><span class="nx">env</span> <span class="p">=</span> <span class="s2">&#34;exec&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">providers</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">store</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">type</span> <span class="p">=</span> <span class="s2">&#34;…&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">secrets</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">DB_PASSWORD</span>    <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;store&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;DB_PASSWORD&#34;</span><span class="p">,</span>    <span class="nx">if_missing</span> <span class="p">=</span> <span class="s2">&#34;error&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nx">PAYMENT_API_KEY</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;store&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;PAYMENT_API_KEY&#34;</span><span class="p">,</span> <span class="nx">if_missing</span> <span class="p">=</span> <span class="s2">&#34;error&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nx">SIGNING_KEY</span>    <span class="p">=</span> <span class="p">{</span> <span class="nx">provider</span> <span class="p">=</span> <span class="s2">&#34;store&#34;</span><span class="p">,</span> <span class="nx">value</span> <span class="p">=</span> <span class="s2">&#34;SIGNING_KEY&#34;</span><span class="p">,</span>    <span class="nx">if_missing</span> <span class="p">=</span> <span class="s2">&#34;error&#34;</span> <span class="p">}</span>
</span></span></code></pre></div><p>The <code>type</code> is the only line that&rsquo;s specific to you, and I&rsquo;ve left it blank on purpose, because which store you use is the least interesting decision in the whole setup. fnox speaks to <a href="https://developer.hashicorp.com/vault">Vault</a>, <a href="https://developer.1password.com/">1Password</a>, <a href="https://github.com/getsops/sops">SOPS</a>, and the managed secret stores of the big clouds. We took the one our provider already gave us, for the boring reason that we were paying for it and it already had access control we trusted. Everything else in the file is the same whichever way you go.</p>
<p>What that file buys:</p>
<p>The repository documents its own secrets. Every name a service needs is listed in one place, in git, with review history. New secret? It&rsquo;s in the file, it arrives with <code>git pull</code>, and the next run either finds it or fails loudly because of that <code>if_missing = &quot;error&quot;</code>. Nobody has to be told anything.</p>
<p>Rotation stops being an event. Change the value in the store; every machine picks it up on its next run. There&rsquo;s no fleet of stale copies.</p>
<p>Access goes back to being an authorization question. Whether you can run against a given environment is decided by your identity in the secret store, not by whether someone remembered to forward you a file. Revoking someone becomes a permissions change instead of a hope that they delete a file.</p>
<p>And there&rsquo;s a dry run. <code>fnox check</code> tells you whether every declared secret resolves for you, without printing any of them. It&rsquo;s the fastest possible answer to &ldquo;is it me or is it broken&rdquo;, which is the question that eats onboarding days.</p>
<p>Then one deliberate restraint. That <code>env = &quot;exec&quot;</code> line means secrets are never injected into an interactive shell, only into the environment of a child process fnox launches. fnox will happily do the other thing; there&rsquo;s a mode that puts everything into your shell the moment you enter the directory. We don&rsquo;t use it. I don&rsquo;t want every process started from that directory to inherit production-adjacent credentials, and that includes editors, and it includes the coding agents I have running in there most of the day. Scoping secrets to <code>fnox exec -- &lt;command&gt;</code> is the whole difference between &ldquo;the build can see the signing key&rdquo; and &ldquo;everything on my machine can see the signing key&rdquo;.</p>
<p>That last part is what I&rsquo;d underestimated. With nothing on disk and nothing in the shell, a secret only exists inside a process that something deliberately started, which turns &ldquo;what can the agent reach&rdquo; into an ordinary permission decision. Deny it <code>fnox</code> and the handful of tasks that call it, allow it everything else, and it can read the repository, build, test and format without a credential ever entering its context.</p>
<p>Then the hole, which I&rsquo;d rather name than have someone else name for me. fnox reads the store as <em>me</em>, using credentials that are sitting on my machine regardless. An agent with a shell doesn&rsquo;t need fnox at all; it can query the store with the provider&rsquo;s own CLI. Denying <code>fnox</code> on its own closes the front door of a building with no walls. What does the real work is that the same allowlist doesn&rsquo;t include that CLI, and that what&rsquo;s within reach from a dev machine is stage rather than production. An agent that can edit <code>.mise.toml</code> can also write itself a task that shells out to anything. The deny rule is a fence, not a proof.</p>
<p>But hold it against what it replaces: a <code>.env</code> file sitting in the working directory in plaintext, readable by anything that can call <code>cat</code>, which is every agent anyone has ever run. Runtime-only injection plus a deny rule is, as far as I can tell, about as good as this currently gets.</p>
<h2 id="where-the-two-halves-meet">Where the two halves meet<a class="gn-heading-anchor" href="#where-the-two-halves-meet" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Individually these are two decent tools. Together they remove a category of manual step.</p>
<p>Because mise tasks are just commands, a task can be <code>fnox exec -- something</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">gradle</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;The escape hatch from earlier, now with the build secrets loaded&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;fnox exec -- ./gradlew&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">secrets</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Check that every build secret resolves for you&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;fnox check&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="s2">&#34;serve:stage&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Run locally against stage secrets (needs cloud access)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">depends</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;stack:up&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s2">&#34;fnox -c fnox.stage.toml exec -- mvn spring-boot:run&#34;</span>
</span></span></code></pre></div><p>Now <code>mise run gradle :app:assembleRelease</code> is a signed release build, and the person running it never types the word fnox, never learns which of the build&rsquo;s inputs are secret, and never holds a copy of any of them. Nobody has to remember which commands need secrets. The task knows.</p>
<p>There&rsquo;s one in there for anyone who lives in an IDE:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tasks</span><span class="p">.</span><span class="nx">ide</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;Launch the IDE with the build secrets in its environment (macOS)&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">run</span> <span class="p">=</span> <span class="s1">&#39;fnox exec -- open -na &#34;Android Studio&#34; --args &#34;$PWD&#34;&#39;</span>
</span></span></code></pre></div><p>An IDE knows nothing about any of this, so a build started from inside it can&rsquo;t sign. Launch the IDE <em>through</em> fnox and its Gradle inherits the environment. It&rsquo;s one line, and it&rsquo;s the difference between a working release build and a signing error nobody can explain, so it stays in the file. I don&rsquo;t reach for it much myself: most of my day is a terminal, a prompt and an agent doing the typing. That&rsquo;s a preference, not a recommendation, and the task is there for whoever wants the other thing.</p>
<p>Then there&rsquo;s the split by environment. One file each, the same variable names in all of them, different values behind those names:</p>
<pre tabindex="0"><code>fnox.toml         # local
fnox.stage.toml   # stage
fnox.prod.toml    # production
</code></pre><p>A task points at the file it needs. Anything that belongs to an environment goes in that environment&rsquo;s file, so nobody can pair stage credentials with local settings by accident.</p>
<p>The local one is the file I&rsquo;d point at if you only read one. It declares almost no secrets, because everything a local run needs already has a harmless default committed in the repository: a local database, a mock in front of anything external, obviously fake keys for the paid services. A fresh clone boots and serves for someone with no access to the secret store at all, and anything that would reach a real provider fails authentication rather than doing something expensive. Access buys you <em>stage</em>. It isn&rsquo;t the price of entry.</p>
<h2 id="committing-references-means-scanning-for-values">Committing references means scanning for values<a class="gn-heading-anchor" href="#committing-references-means-scanning-for-values" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Committing secret references only works if you&rsquo;re sure nothing real ever slips in beside them. So the same setup carries a scanner, and mise installs it without anybody being asked to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tools</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">gitleaks</span> <span class="p">=</span> <span class="s2">&#34;latest&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">hooks</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">postinstall</span> <span class="p">=</span> <span class="s2">&#34;git config core.hooksPath .githooks&#34;</span>
</span></span></code></pre></div><p><code>mise trust</code> is the first thing anyone runs, and the first <code>mise run</code> after it installs the toolchain, which is also what fires the postinstall hook and points git at a directory holding a pre-commit secret scan. It arrives as a side effect of the step they were taking anyway, rather than a separate instruction that people skip.</p>
<p>The hook scans staged content only, so it&rsquo;s fast, and if mise isn&rsquo;t on the PATH it skips with a loud warning instead of failing. A GUI git client&rsquo;s environment is often minimal, and blocking someone&rsquo;s commit over a PATH problem is a good way to teach them to pass <code>--no-verify</code> forever. CI scans the same commits again after the push.</p>
<h2 id="and-the-deployments">And the deployments<a class="gn-heading-anchor" href="#and-the-deployments" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>People tend to assume this bit works differently than it does.</p>
<p>Our deployed environments don&rsquo;t run fnox. The platform they run on reads the same values from the same store itself and hands them to the process as ordinary environment variables. No application code knows about any of this. Nothing in the runtime imports a library, and no build has to be told which environment it&rsquo;s for.</p>
<p>That&rsquo;s deliberate. The store is the source of truth. fnox is how a <em>human</em> reads that truth, with their own credentials, on their own machine, and the deployment reads the same truth with the machine&rsquo;s identity. The two never diverge, and the application on both sides just sees environment variables like it always did.</p>
<h2 id="what-it-doesnt-fix">What it doesn&rsquo;t fix<a class="gn-heading-anchor" href="#what-it-doesnt-fix" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The honest version, then.</p>
<p>You still have to install mise, and put <code>mise activate</code> in your shell profile. That&rsquo;s a real setup step and I&rsquo;m not going to pretend it away. It&rsquo;s one step, it&rsquo;s the same step in all sixty-eight repositories, and it never goes stale, which is three things the paragraph it replaced could not say.</p>
<p>Docker is still yours. mise installs tools, not daemons. Our local stacks need containers, and the first-run experience for anyone without Docker configured is still a manual step and a download measured in gigabytes. Same for Xcode, which remains its own weather system.</p>
<p>Getting the tooling right removes the stupid part of a cold start. It does nothing about the hard part, which is understanding what the service actually does. That&rsquo;s still reading code, and it still takes days.</p>
<p>You&rsquo;re adding two dependencies to every developer&rsquo;s day. mise is mature and widely used. fnox is young. We&rsquo;ve committed real workflows to it, and if it stalls we&rsquo;d be doing a migration. I think that&rsquo;s a reasonable bet: the file is declarative, the values live in a store we&rsquo;d keep either way, and the fallback is a shell script that reads the same parameters. But it is a bet, and pretending otherwise would be dishonest.</p>
<p>One line in that config deserves a careful review. fnox can be told to run a command that logs you in to the store when your session has expired, which is genuinely convenient and also means a committed config file now specifies something that executes on every reviewer&rsquo;s laptop. Review that line the way you&rsquo;d review a CI workflow.</p>
<p>There&rsquo;s also a failure mode with a misleading error. Point the provider at the wrong account and what comes back is &ldquo;not found&rdquo;, which reads like a missing secret rather than a wrong identity, and sends people looking in entirely the wrong place. Both of our files carry a comment naming exactly which account the credentials have to resolve to.</p>
<h2 id="where-we-actually-are">Where we actually are<a class="gn-heading-anchor" href="#where-we-actually-are" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two repositories fully on fnox. A handful more on mise. Sixty-odd to go, most of which will never need the secrets half at all.</p>
<p>I haven&rsquo;t put a stopwatch on it and I&rsquo;m not going to invent a figure I didn&rsquo;t measure. The shape of the answer is clear enough though. On a machine that&rsquo;s been through this once already, with mise installed and the toolchain cached and Docker running, a clone to a serving API is under a minute. Comfortably. What&rsquo;s left of the old two-to-six hours is all first-time-on-this-laptop cost: downloading a JDK, pulling images, installing mise itself. You pay that once, rather than once per repository, and that second part is the whole difference from where we started.</p>
<p>In the repositories where this landed, the setup section of the README went from a list of things to install and a paragraph about asking a colleague down to this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mise trust          <span class="c1"># once, to allow this repository&#39;s config</span>
</span></span><span class="line"><span class="cl">mise run serve
</span></span></code></pre></div><p>Two lines, and the second one is the one you wanted anyway. The first is mise asking whether you actually trust a config file that&rsquo;s about to install software and set environment variables, which, given that this file can now run commands on your machine, is a prompt I&rsquo;m glad exists.</p>
<p>The pattern generalises well past our particular stack, which is why I think it&rsquo;s worth writing down rather than filing as an internal chore.</p>
<p>Then delete the setup guide, because it&rsquo;s finally lying about how much work there is.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Infrastructure</category>
    </item>
    <item>
      <title>Four products, sixty-eight repositories</title>
      <link>https://build.golfnext.com/en/posts/four-products-68-repos/</link>
      <pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/four-products-68-repos/</guid>
      <description>Two weeks inside GolfNext. What we actually build, what thirteen years of software looks like underneath it, the integrations we started in week two, and an honest look at the goals I set myself before I&#39;d signed anything.</description>
      <content:encoded><![CDATA[<p>I started at GolfNext on Monday the 3rd of August, at 8:00. I&rsquo;d pictured a quiet office and twenty minutes of standing around. Most of the team was already in and in a good mood about it. I&rsquo;d brought a cheesecake, which we ate after lunch, because I&rsquo;ve been in Denmark long enough to know that turning up with cake is not really optional here.</p>
<p>Peter, who started the company in 2012, had Claude open before I&rsquo;d found my desk, working through the accounts and permissions I&rsquo;d need. Small thing to notice on a first morning. It told me more than an onboarding deck would have.</p>
<p>The first post on this blog was a letter I wrote to myself before I&rsquo;d signed anything, full of predictions about a company I&rsquo;d visited once. This is where I hold it up against what&rsquo;s actually here. Most of what follows isn&rsquo;t really about me though. It&rsquo;s the company I want to describe: what we build, what sits underneath it after thirteen years, and what we&rsquo;re planning to do about that.</p>
<h2 id="what-golfnext-builds">What GolfNext builds<a class="gn-heading-anchor" href="#what-golfnext-builds" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>If you&rsquo;ve never thought about the software a golf club runs on, here&rsquo;s the shape of it. A club is a small business wearing several hats at once. There&rsquo;s a pro shop, a driving range, memberships, a booking system, and more and more often a simulator bay or two. The booking system handles tee times, which, since I had to learn this myself, have nothing to do with tea. A tee time is the slot a group is booked to start their round on the first hole, and the whole day at a club is arranged around that grid.</p>
<p>Historically each of those hats arrived as its own box, from its own vendor, with its own software attached. Buy the dispenser, get the dispenser company&rsquo;s system, whether or not it suits how you run your club.</p>
<p>Our position is the opposite of that. Your hardware and your infrastructure stay yours, and you stay free to pick the software that fits. We want to be open to any integration, so whatever management system a club already uses to run its tee times, the aim is to work with it rather than ask them to throw it out. Freedom to choose, power to connect.</p>
<p><strong>Self-service kiosks.</strong> A digital reception that runs around the clock. Green fees, rentals, pro shop purchases, all without someone standing behind a counter. As far as we know it&rsquo;s the only one of its kind in golf. A club gets fewer staffed hours and keeps earning outside opening times. A player gets the same convenience they already have everywhere else in their life.</p>
<p><strong>Driving range payments.</strong> Payments on the range that work with any ball dispenser brand. That compatibility is the product. The dispenser belongs to the club, or to whoever sold it to them, and being able to sit on top of all of them is the whole selling point. Cards, apps, wallets, PIN codes, with real-time analytics behind it. In a lot of places the range still runs on cash, and this is where demand is growing fastest right now. More than 500 million balls a year go through it. That&rsquo;s a lot of slices.</p>
<p><strong>Voucher management.</strong> Issuing, distribution and redemption, all digital, from a single ticket up to a full sponsor package.</p>
<p><strong>Simulator management.</strong> Automated access control, so a simulator only runs during a paid session. No more time theft, better utilisation.</p>
<p>Where that adds up to today: 400+ venues, 18 countries, 350,000+ active users. Founded in Denmark in 2012 and still very Danish, which is also why I walked out of my first day having decided to get serious about my Danish.</p>
<h2 id="the-stack-without-the-polish">The stack, without the polish<a class="gn-heading-anchor" href="#the-stack-without-the-polish" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Sixty-eight repositories. JavaFX on the desktop side, PHP, Vue, React, React Native, Spring, Java, Kotlin. All of it on AWS.</p>
<p>Written out like that it looks like indecision, and it isn&rsquo;t. Thirteen years, four product lines, hardware sitting in the field, and for most of that history the software was built by external partners who were good at their jobs. Every project picked what made sense on the day it started, and most of those calls were reasonable ones. What never happened is anybody going back afterwards to make the choices agree with each other.</p>
<p>So, the honest state, because I said in the letter I&rsquo;d write these things down rather than smooth them over.</p>
<p>A good share of those sixty-eight repositories hadn&rsquo;t been touched in years and were never archived. Forks, spikes, one-off tools that outlived their reason. I started archiving in the first week, slowly, because for most of them I didn&rsquo;t yet know what I was looking at, and not knowing is a much better reason to leave something alone than to delete it.</p>
<p>In the code you can read the generations. Several package structures inside the same language, none of them wrong, all of them different. An <code>AGENTS.md</code> written for Codex and a <code>CLAUDE.md</code> written for Claude, sitting in the same repository, describing the same project, disagreeing about it. A <code>README.md</code> documenting a version of the system that doesn&rsquo;t exist anymore. A runtime that went end of life a while back and is still running. And a release process that&rsquo;s properly good for some of our tools and a snowflake for others, where part of it lives in a pipeline and part of it lives in somebody&rsquo;s head.</p>
<p>Security sits at the top of the pile. Of the twenty-nine engineering practice items I&rsquo;ve written down so far, five are marked high priority, and all five are security. That&rsquo;s not a discovery about GolfNext in particular. It&rsquo;s what thirteen years of shipping tends to look like anywhere nobody has recently been paid to go and look.</p>
<p>Since I made a prediction about all this from the outside, I owe you the result. It&rsquo;s roughly where I expected it to be, and I mean that as a fair outcome rather than a complaint. This is thirteen years of continuous delivery by people shipping features that customers actually needed, on deadlines, with the priorities they were handed at the time. You&rsquo;d find much the same picture at most companies this age, and I&rsquo;d rather be the person who tidies it than the person who tuts at it. The one prediction that landed exactly was the dull one: I&rsquo;d guessed from outside that the repos would be under-automated, and they are. That&rsquo;s my opening.</p>
<h2 id="the-part-i-got-wrong">The part I got wrong<a class="gn-heading-anchor" href="#the-part-i-got-wrong" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I&rsquo;d braced myself for the products to be the weak spot. Companies carrying a stack like ours often have shipped-anyway software on top, the kind where you can feel the deadline in the interface.</p>
<p>That&rsquo;s not what&rsquo;s here. The products are good. There&rsquo;s room in all of them, obviously. But the thing I keep noticing in meetings is that everybody reasons from the customer&rsquo;s side first, including people with no engineering background at all. What does the player standing at the kiosk at half five in the morning actually experience. What does a club manager have to explain to a member when something goes wrong.</p>
<p>That instinct is hard to retrofit into a team. Package structures can be refactored and READMEs can be rewritten, and both of those are just work. Teaching a group of people to care about the person at the other end is a different order of problem, and we don&rsquo;t have it.</p>
<h2 id="what-we-started-in-week-two">What we started in week two<a class="gn-heading-anchor" href="#what-we-started-in-week-two" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Two integrations landed almost immediately, and they&rsquo;re the same bet in different clothes. The platform is only open if it actually connects to the systems clubs already run.</p>
<p><strong>teeControl</strong> is a management system a lot of clubs use, and we&rsquo;re integrating it so it works through our kiosks. First impressions are good. First impressions are also cheap, and there&rsquo;s a fair way to go from here. We need proper test systems. We&rsquo;ll probably have to adjust some of our own flows to fit theirs. There are decisions still to settle on both sides. That&rsquo;ll be a process rather than a sprint, and I&rsquo;d rather say so now than write a triumphant post about it in week two.</p>
<p><strong>Lightspeed</strong> is a player-facing app we want to support as a payment option. Peter had already built a first version before I arrived, and taking it over was my excuse to get my hands into real code here for the first time. Ten thousand lines, read end to end, then run and poked at until I understood what it was doing. That produced a list of bugs and a stack of follow-up stories, which is what reading anything carefully produces. The more useful outcome is that it&rsquo;s in my head now as well as his.</p>
<p>And <strong>Verifone</strong> terminals on the desk, which is the regular reminder that a lot of our software ends in a physical object somebody has to touch.</p>
<p>The approach John and I settled on is to skip the big upfront specification. John is the other developer, though that undersells him, since he&rsquo;s as much solution architect and project manager as engineer. Get a test instance, build against it, let reality write the documentation. Two weeks in I don&rsquo;t know these systems well enough to write a good spec, and pretending otherwise would just produce a document we&rsquo;d quietly stop reading by September.</p>
<p>One small thing worth passing on to anyone thinking about working somewhere like this. I spent my first few days being patient about access, on the assumption there was a process I was supposed to wait for. Then I said out loud that this was more or less what I&rsquo;d been hired for, and could I please just have it. Half an hour later I had AWS. The waiting was entirely my own invention.</p>
<h2 id="how-we-ship">How we ship<a class="gn-heading-anchor" href="#how-we-ship" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The most interesting problem of the first two weeks wasn&rsquo;t inside a repository. It was working out how changes get out to customers.</p>
<p>I spent a Friday on proper detective work. CloudWatch logs on one screen, a downloaded database snapshot on the other, lining transaction states up against what the code believes should have happened. Somewhere in there I found forty commits that had never been pushed anywhere. This is the unglamorous half of bringing knowledge in-house. Credentials are the easy part. Rebuilding an understanding of what the system actually does takes a lot longer.</p>
<p>We don&rsquo;t have a gradual rollout mechanism today. What we do have is customers who are physical places, and a handful of them are a short drive from the office. So the first ring is geography. A few nearby clubs get the new version first, and if it goes wrong the recovery plan is twenty minutes in a car with a spare terminal in the boot. That took about five minutes of conversation to arrive at, because once you notice where the customers are it&rsquo;s a fairly obvious move. It&rsquo;s not what I&rsquo;d draw on a whiteboard. It&rsquo;s available this week, and it buys enough confidence to keep shipping while the proper thing gets built.</p>
<h2 id="learning-the-game-and-the-words-for-it">Learning the game and the words for it<a class="gn-heading-anchor" href="#learning-the-game-and-the-words-for-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>On day three I played the first round of golf of my life, with colleagues. Seven or eight shots a hole. Never a ten. We didn&rsquo;t lose a single ball all round, which I&rsquo;m told is the more impressive statistic for a beginner. Nobody wrote down a score.</p>
<p>I&rsquo;d recommend the general version of that to anyone joining a company whose domain they don&rsquo;t know. Not golf specifically. Going and doing the thing the software is about.</p>
<p>The week after, John and I sat down for what turned into the most useful two hours of week two: agreeing what we call things, before three systems each invent their own word for the same concept. Nine holes is a loop. Stroke index ranks the holes by difficulty. There&rsquo;s rating category. None of this is exotic if you play golf, and all of it is invisible to an engineer who has never stood on a tee box, which two weeks ago was me.</p>
<p>Getting the vocabulary right early costs almost nothing. Getting it wrong costs you three services, three names for one thing, and a translation layer nobody set out to build. I did run one check I&rsquo;d suggest to anyone doing this: I had Claude verify &ldquo;loop&rdquo; on its own, because it smelled to me like a word engineers invent and then talk themselves into believing is domain language. It came back as real golf. Using the model to check our own vocabulary, rather than to write code, is one of the better uses I&rsquo;ve found for it so far.</p>
<h2 id="twenty-nine-items-and-where-this-is-going">Twenty-nine items, and where this is going<a class="gn-heading-anchor" href="#twenty-nine-items-and-where-this-is-going" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The improvement list lives in ClickUp now, mostly so I can stop carrying it around in my head. Twenty-nine items, all still in the backlog, grouped roughly into repository standards and conventions, security and secrets, CI/CD and testing and dependencies, infrastructure and data, architecture direction, and AI and agents.</p>
<p>The first thing we&rsquo;ve actually rolled out is the least exciting item on the whole list. <a href="https://mise.jdx.dev/">mise</a>, which pins and installs the right language versions per project, so you clone a repo and the correct Java turns up on its own. With that many technologies spread across that many repositories, the tax you pay all day long is switching between them, and that particular tax is avoidable.</p>
<p>The goal hasn&rsquo;t changed since the letter. Build a landscape where context is cheap to pick up. If a new engineer, or a fresh AI session, can understand a service quickly, work can start immediately, by whoever picks it up. If they can&rsquo;t, every task starts with archaeology. Everything on that list of twenty-nine sits downstream of that one sentence.</p>
<h2 id="the-scorecard">The scorecard<a class="gn-heading-anchor" href="#the-scorecard" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Which brings me back to the letter, and the promise to mark my own homework in public.</p>
<p><strong>Stay on the keys.</strong> Held. Two terminal fixes shipped, pull requests reviewed, and I&rsquo;ve taken ownership of one of the apps. I&rsquo;m still building, which was the condition I set before I accepted the job.</p>
<p><strong>Rebuild the understanding, not just the access.</strong> Mostly. The CloudWatch afternoon, the database snapshot, the Lightspeed handover: that&rsquo;s the right shape of work. What I can&rsquo;t claim yet is being able to release anything in the stack without asking somebody.</p>
<p><strong>Candor in the open.</strong> Held, and easier than I expected, mostly because the room here makes it easy.</p>
<p>Now the other half.</p>
<p><strong>Goal one slipped.</strong> &ldquo;Make the work visible&rdquo; was first on the list and this blog was supposed to be how. Every post before this one was written before I&rsquo;d signed the contract, which turns out to be the easiest possible time to write about a company. Then I actually started, and for two weeks there were things in front of me that mattered more than a blog post. I&rsquo;m not going to call that a crisis. I did write that if it went quiet I should treat it as a warning sign, so: noted.</p>
<p><strong>The cold start is nowhere near five minutes.</strong> I set myself five minutes from clone to running code and told future me to go and time it. I haven&rsquo;t timed it properly, which is its own small failure. I&rsquo;ve cloned enough of these repositories by now to give a fair estimate though. Somewhere between two and six hours depending which one you pick, call it four on average. Getting the iOS build running took most of a day of Xcode, pods and emulators. mise is the first real step at closing that, and a step isn&rsquo;t a number.</p>
<p><strong>The shared AI kit is item twenty-nine of twenty-nine, and that part is on purpose.</strong> In the letter I called it the thing that turns &ldquo;AI-first&rdquo; from a slogan into shared tooling. Two weeks in I can see why it can&rsquo;t come first. A shared kit built on top of sixty-eight inconsistent repositories would mostly just encode the inconsistency. Foundation first, kit second, and I&rsquo;d defend that order. What I won&rsquo;t pretend is that the order is free. Until we get there, I&rsquo;m getting a lot of leverage out of a setup I&rsquo;ve spent a year tuning that nobody else here has yet.</p>
<p><strong>And the days ate the space to think.</strong> I wrote that the creative, playful time isn&rsquo;t a reward you earn once the serious work is done, that it <em>is</em> the serious work. Two weeks in, it&rsquo;s the first thing the calendar takes. One thing helps here, and it&rsquo;s specific to this industry: golf is seasonal. Clubs are busy now, so we&rsquo;re busy now. The platform goes quiet over the winter, and that&rsquo;s when the toolset for the 2027 season gets built. Having a shape to the year is more useful than I expected it to be.</p>
<p>The thing I got most wrong is also the one I&rsquo;m happiest about. I turned up braced for friction. I expected to have to earn access, argue for scope, make a case for every change. Almost none of that was there, and what resistance I did meet in the first fortnight I&rsquo;d brought with me.</p>
<p>Two weeks isn&rsquo;t long enough to conclude much. Ask me again in the winter, when the clubs go quiet and we find out what we actually do with the time.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Product</category>
      <category>Culture</category>
    </item>
    <item>
      <title>Making Claude Code shut up (and other customizations that earned their keep)</title>
      <link>https://build.golfnext.com/en/posts/tuning-claude-code/</link>
      <pubDate>Wed, 24 Jun 2026 10:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/tuning-claude-code/</guid>
      <description>The first thing I did with Claude Code was make it talk less. The small, shareable customizations that survived my own filter — terse output, a statusline, the right hooks — plus the ones I deliberately refuse to automate, and why managing the context window shouldn&#39;t be a skill at all.</description>
      <content:encoded><![CDATA[<p>The first thing I did with <a href="https://code.claude.com/docs">Claude Code</a> was make it talk less.</p>
<p>Claude Code is Anthropic&rsquo;s terminal coding assistant — you talk to it, and it reads, writes, and runs code in your project. Out of the box it&rsquo;s polite. &ldquo;Certainly! I&rsquo;d be happy to help you with that. The issue you&rsquo;re seeing is likely caused by&hellip;&rdquo; I read fast, I pay per token, and I don&rsquo;t need the warm-up. I switched that off. I run a community plugin, <a href="https://github.com/JuliusBrussee/caveman">caveman</a>, that strips responses to the bone: no articles, no pleasantries, every bit of technical substance intact. &ldquo;Bug in auth middleware. Token expiry check uses <code>&lt;</code> not <code>&lt;=</code>. Fix:&rdquo; — and then the fix.</p>
<p>It takes adjusting. Some of those compressed sentences make you stop and read twice; a fragment can carry more meaning than it first lets on, and you spend the saved second decoding it. But the trade lands well in my favor. I get to the point faster, the context window fills slower, and if a line is genuinely unclear there&rsquo;s always the escape hatch: ask it to expand. It never minds.</p>
<p>That mode is the smallest possible example of the thing this whole post is about. Claude Code out of the box is generic. It&rsquo;s built to be reasonable for everyone, which means it&rsquo;s optimal for no one. The leverage isn&rsquo;t the model itself — you can&rsquo;t change how good it is, only which one you run. The leverage is in bending the tool around your own loop.</p>
<h2 id="the-trap">The trap<a class="gn-heading-anchor" href="#the-trap" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Customization isn&rsquo;t free, and I want to be honest about the bill. You can pour weeks into a setup nobody else can read, including future you.</p>
<p>The tools that took configurability furthest are the ones everyone reveres: vim and git. Deep, powerful, endlessly tunable. And, I&rsquo;d argue gently, not actually that accessible. The depth is both the point and the price. Vim is the only tool I know with a meme about how to quit it — that should tell us something.</p>
<p>I hear the objection most about git: &ldquo;but git is so <em>easy</em>.&rdquo; Is it, though? It&rsquo;s easy right up until your first real merge conflict, or the afternoon you rebase the wrong thing and quietly rewrite your own history with no clean way back. Yes, you can see where you&rsquo;re trying to get to. So can someone holding the box for a fifty-thousand-piece puzzle, the finished picture printed right there on the lid. Seeing the goal is not the same as the path being accessible. Those are different claims, and we keep mixing them up.</p>
<p>Here&rsquo;s the belief, stated plainly instead of as a complaint: a great tool should be as accessible as it can possibly be, and <em>then</em>, having earned that, offer infinite depth to anyone who wants to dig. The accessible part is the whole job, not a consolation prize for beginners. Depth is the bonus you add on top.</p>
<p>My bar for any Claude Code customization is one question: <em>would I hand this to a teammate without an hour of briefing?</em> If the answer is no, it usually doesn&rsquo;t survive. My stack stays simple and stupid on purpose. Here&rsquo;s what&rsquo;s left after that filter.</p>
<h2 id="what-earned-a-slot">What earned a slot<a class="gn-heading-anchor" href="#what-earned-a-slot" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p><strong>The terse output mode.</strong> Covered above. The single highest-leverage change, because it touches every response. Less to read, fewer tokens burned, longer-lived context.</p>
<p><strong>The statusline.</strong> This matters more than it sounds. I usually have several things going at once, and I lose track of where I am — which repo, which task, how much budget I&rsquo;ve got left. My statusline shows it all in one strip: the model I&rsquo;m on, how full the context window is, where I stand against my 5-hour usage limit (Claude Code caps how much you can run in any rolling five-hour window), and my current directory. No clicking, no guessing. I glance down and I know my situation. For anyone juggling more than one thing at a time, it&rsquo;s the cheapest sanity I&rsquo;ve bought.</p>
<p><strong>The settings layer.</strong> This is where it gets personal, and where the options never end. My config plays a sound when Claude finishes so I can look away and get pinged back. It fires OS notifications. You can have it log the purpose of each session, track tokens and cost, whatever you decide is worth knowing. There&rsquo;s no &ldquo;right&rdquo; setup — the point is that the hooks exist (little commands the tool runs automatically at set moments, like when it finishes) and you bend them to <em>your</em> purpose. Mine is &ldquo;tell me when you&rsquo;re done.&rdquo; Yours might be something else entirely.</p>
<p>None of these is clever. That&rsquo;s the point. Each one removes a small, daily friction, and none of them needs explaining.</p>
<h3 id="the-actual-wiring">The actual wiring<a class="gn-heading-anchor" href="#the-actual-wiring" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h3>
<p>The sound is a <code>Stop</code> hook in <code>settings.json</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;hooks&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;Stop&#34;</span><span class="p">:</span> <span class="p">[{</span> <span class="nt">&#34;hooks&#34;</span><span class="p">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;afplay ~/.claude/sounds/done.mp3&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;async&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}]}]</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p><code>async: true</code> so it never blocks the session, and <code>afplay</code> is just the built-in macOS player — swap in whatever you like.</p>
<p>The statusline is barely more than that. Claude hands your script a blob of JSON on every render; the script prints a line. You point at it once:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="s2">&#34;statusLine&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;command&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;command&#34;</span><span class="p">:</span> <span class="s2">&#34;bash ~/.claude/statusline.sh&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>and the script reads whatever fields it cares about off stdin:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">input</span><span class="o">=</span><span class="k">$(</span>cat<span class="k">)</span>                                                  <span class="c1"># session state, as JSON</span>
</span></span><span class="line"><span class="cl"><span class="nv">model</span><span class="o">=</span><span class="k">$(</span>jq -r <span class="s1">&#39;.model.display_name&#39;</span>                          <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">ctx</span><span class="o">=</span><span class="k">$(</span>  jq -r <span class="s1">&#39;.context_window.used_percentage // 0 | floor&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">fh</span><span class="o">=</span><span class="k">$(</span>   jq -r <span class="s1">&#39;.rate_limits.five_hour.used_percentage // 0 | floor&#39;</span> <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">dir</span><span class="o">=</span><span class="k">$(</span>  jq -r <span class="s1">&#39;.workspace.current_dir&#39;</span>                       <span class="o">&lt;&lt;&lt;</span><span class="s2">&#34;</span><span class="nv">$input</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span></code></pre></div><p>From there it&rsquo;s just drawing strings. Mine renders a little colored bar for each percentage — green, then yellow, then red as it fills — and right-aligns the path, so the strip ends up looking like this:</p>
<pre tabindex="0"><code>Opus 4.8  xhigh  · CTX ████░░░░░░ 38%  · 5h ██░░░░░░░░ 21%        ~/dev/golfnext
</code></pre><p>(<code>xhigh</code> there is just the reasoning-effort level I keep the model at.) That&rsquo;s the whole thing. No framework, no dependency to keep current. A shell script and a JSON blob.</p>
<h2 id="what-i-wont-automate">What I won&rsquo;t automate<a class="gn-heading-anchor" href="#what-i-wont-automate" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>More automation isn&rsquo;t always a win. Some of it just moves the cost somewhere you can&rsquo;t see, and those are the ones I leave switched off.</p>
<p>You can wire Claude to lint and format every file the moment it edits it. Sounds tidy. It&rsquo;s the opposite. If a hook reformats a file right after the model touched it, the model&rsquo;s picture of that file is now stale — so the next time it works there, it has to re-read the whole thing to catch up. You&rsquo;ve traded a clean diff for a slower, token-hungrier session. I&rsquo;d rather it just run the linter once, at the very end, when the thinking is done.</p>
<p>Same logic on automatic model fallback. You can have it silently switch to a smaller model when your first choice is busy. I don&rsquo;t want that. I pick a model deliberately, usually because the task needs the bigger reasoning model — the one that pauses to work a problem through before answering. If it&rsquo;s overloaded, I&rsquo;d rather come back in two hours and write &ldquo;pick this up again&rdquo; than have something quietly hand my hard problem to a model that can&rsquo;t carry it. Convenience isn&rsquo;t worth a worse answer I didn&rsquo;t notice I was getting.</p>
<h2 id="the-window-should-be-a-non-event">The window should be a non-event<a class="gn-heading-anchor" href="#the-window-should-be-a-non-event" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>People talk a lot about managing the context window — compacting it (having the tool summarize the conversation so far to free up room), watching it, nursing it along. I try to never be in that conversation.</p>
<p>The way I avoid it is structural: push the heavy lifting onto subagents — separate helper instances of Claude Code, each with its own private context — and keep the main thread clean. A subagent goes off, burns its own context reading and searching, and hands back the short answer. The main session stays light. Done right, I almost never reach for compaction, because I never let the window get full in the first place.</p>
<p>But here&rsquo;s the honest part: I think the fact that any of us has to know this is a smell. Nobody should <em>need</em> to understand context windows to start being productive with this tool. It&rsquo;s a current limitation wearing the costume of a skill. A few years from now I&rsquo;d bet it&rsquo;s abstracted away entirely, and we&rsquo;ll talk about it the way we talk about managing our own RAM — which is to say, we don&rsquo;t. I optimize around it today, and I try not to mistake working around a limitation for something worth teaching.</p>
<p>Can I prove it? Not with numbers. I don&rsquo;t have a clean before-and-after, and I distrust anyone who quotes a tidy percentage for this. What I have is a feel: the window fills slower, the budget stretches further, sessions stay light longer. It&rsquo;s subjective, and I&rsquo;d notice the moment it was gone.</p>
<h2 id="how-to-find-your-own">How to find your own<a class="gn-heading-anchor" href="#how-to-find-your-own" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The hard part isn&rsquo;t configuring anything. It&rsquo;s knowing what&rsquo;s even possible.</p>
<p>Claude Code moves fast — it feels like ten new commands a week. So fast that the model itself can&rsquo;t keep up. I&rsquo;ll ask it about a real slash command — &ldquo;what does <code>/goal</code> do, can you explain?&rdquo; — and it has no idea the command exists. There&rsquo;s something funny about that: the most capable thing I work with often can&rsquo;t tell you about features that shipped after its training cutoff. It&rsquo;s behind on itself, in the most literal way. You can&rsquo;t just ask the model what it can do. You have to go look — read the changelog, skim a guide, point it at the open web where the fresh stuff actually lives.</p>
<p>The next thing I want to try isn&rsquo;t a setting at all. I keep half a dozen projects in flight, so I&rsquo;m going to build one small meta-repo: a single place that points at all of them, that anyone — not just me — can navigate. Same instinct as everything else here: if it only works for me, it doesn&rsquo;t really work.</p>
<p>That&rsquo;s the whole filter, in the end. Take the two or three things that kill a real, repeated friction. Leave the rest, however clever it looks. The goal was never the most elaborate setup. It was the one you&rsquo;d still be happy to hand to someone else.</p>
]]></content:encoded>
      <category>AI</category>
      <category>Engineering</category>
    </item>
    <item>
      <title>The Loop Debate Is a Bad Word for Autonomy</title>
      <link>https://build.golfnext.com/en/posts/loops-autonomy/</link>
      <pubDate>Wed, 24 Jun 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/loops-autonomy/</guid>
      <description>Everyone&#39;s debating &#34;Claude Code loops.&#34; After tracking a month of real tasks — every token, every hour it would have cost by hand — I&#39;m fairly sure most of the argument is about the wrong word. It&#39;s not loops, it&#39;s autonomy. And like microservices a decade ago, it arrives one verifiable task at a time, gated by a substrate nobody has built yet.</description>
      <content:encoded><![CDATA[<p>There&rsquo;s a genre of post going around right now. You&rsquo;ve probably seen it. &ldquo;I don&rsquo;t write prompts anymore — I design loops.&rdquo; &ldquo;Loop engineering is the new prompt engineering.&rdquo; I read these closely — I&rsquo;m trying to learn this as fast as anyone, and the people writing them are usually well ahead of me. But I keep coming away with the same itch: I can&rsquo;t quite map the word onto what I actually do all day.</p>
<p>I run Claude Code most days. For people who haven&rsquo;t met it: Claude Code is Anthropic&rsquo;s coding agent — you give it a task in the terminal and it reads files, writes code, runs tests, and iterates, instead of just handing you a snippet to paste. The capability is real and I lean on it hard. What trips me up is the vocabulary, so I tried to ground it.</p>
<p>I did the boring thing: I tracked everything. Every real task I ran through Claude Code got two numbers next to it — what it cost in tokens, and a rough estimate of what the same work would have cost by hand, in time. A month of rows. It&rsquo;s the least glamorous artifact imaginable, and it&rsquo;s the thing that finally made the &ldquo;loops&rdquo; debate click into place for me.</p>
<p>What clicked — for me, at least — is that a lot of the argument is about the wrong word. &ldquo;Loop&rdquo; isn&rsquo;t the thing. Autonomy is. And autonomy doesn&rsquo;t arrive as a switch you flip; it arrives one task at a time, which, if you were around for the last big architecture argument, should feel oddly familiar.</p>
<h2 id="loop-is-four-different-things">&ldquo;Loop&rdquo; is four different things<a class="gn-heading-anchor" href="#loop-is-four-different-things" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Part of why the debate feels muddy is that &ldquo;loop&rdquo; is doing at least four jobs at once, and people argue past each other because they&rsquo;re each holding a different one.</p>
<p>There&rsquo;s the <strong>agent loop</strong> — think, act, observe, repeat. This is just how an agent works. It reads the situation, takes an action, looks at the result, decides what&rsquo;s next. It has always existed. It&rsquo;s not a technique, it&rsquo;s the definition.</p>
<p>There&rsquo;s <strong>Ralph</strong> — the bash hack. It&rsquo;s the community&rsquo;s name for the crudest possible loop: wrap the agent in a literal <code>while</code> loop in your shell and let it re-run the same prompt until it declares itself done. (The name is just folklore; the technique is the point.) Crude, occasionally effective, the duct tape of autonomy.</p>
<p>There are the <strong>features</strong> — slash-commands like <code>/loop</code> and <code>/goal</code>, baked into tools like Claude Code. They wrap that same idea in something more controlled than a bash one-liner.</p>
<p>And there&rsquo;s <strong>&ldquo;loop engineering&rdquo;</strong> — the philosophy. The discourse. The posts.</p>
<p>Four different altitudes, one word. No wonder it&rsquo;s confusing.</p>
<p>But here&rsquo;s the thing they all share, the line every one of these repeats in its own dialect: <strong>goal, verification, stop condition.</strong> That trio is the actual substance. Everything else is which layer someone happens to be staring at.</p>
<p>Make it concrete. Take the most boring useful thing I automate: addressing review comments on a pull request.</p>
<ul>
<li><strong>Goal:</strong> resolve every review comment on this PR.</li>
<li><strong>Verification:</strong> CI is green, and every comment thread is actually addressed — not &ldquo;I think I fixed it,&rdquo; but the check passes and the thread is closed.</li>
<li><strong>Stop condition:</strong> no open comments left, or it hits an iteration cap and hands back to me.</li>
</ul>
<p>That&rsquo;s a loop. It&rsquo;s also just a task with a clear definition of done and a rule for when to quit. The trio is the whole game.</p>
<p>And notice what makes that example work: a machine, not me, can tell whether the goal is met — the check is green or it isn&rsquo;t. That&rsquo;s what I&rsquo;ll keep calling <em>verification</em> — an automatic, judgment-free signal that the work is actually done. Hold onto the word. How much of it a task has is the thing that decides everything later.</p>
<h2 id="its-not-loops-its-autonomy">It&rsquo;s not loops. It&rsquo;s autonomy.<a class="gn-heading-anchor" href="#its-not-loops-its-autonomy" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Here&rsquo;s the part it took me a while to see. Read those three criteria again — goal, verification, stop condition — and ask: does anything in them change based on whether <em>I</em> press enter between each step, or a harness (the script or tool wrapping the agent) does it for me? No. Nothing. The goal is the goal, the verification is the verification, the stop condition is the stop condition; they&rsquo;re identical whether a human advances each iteration or a script does. The only variable is <strong>who turns the crank between steps</strong> — and that isn&rsquo;t a question about loops at all. It&rsquo;s a question about autonomy: how much you trust the thing to keep going without you in the chair.</p>
<p>Which makes sense once you see how little there is to a loop — near enough, it&rsquo;s this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">while</span> <span class="ow">not</span> <span class="n">done</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">observe</span><span class="p">()</span>              <span class="c1"># look at where things stand</span>
</span></span><span class="line"><span class="cl">    <span class="n">act_toward</span><span class="p">(</span><span class="n">goal</span><span class="p">)</span>       <span class="c1"># take the next step</span>
</span></span><span class="line"><span class="cl">    <span class="n">done</span> <span class="o">=</span> <span class="n">verified</span><span class="p">()</span>      <span class="c1"># the stop condition: is the goal actually met?</span>
</span></span></code></pre></div><p>That&rsquo;s the whole thing. So &ldquo;should I be designing loops&rdquo; is a bit of a category error — the loop is trivial, a <code>while</code> and an <code>if</code>. The hard parts hide behind the word, and there are two of them. The first: how far are you willing to step back from a running process? That&rsquo;s trust. The second, the one I almost never see named: once you <em>do</em> step back, where does the thing actually run, and can you afford to run it there? That&rsquo;s infrastructure — and unlike trust, it isn&rsquo;t really up to you yet, because the cheap, standard place to run autonomous agents doesn&rsquo;t exist.</p>
<h2 id="what-the-work-actually-costs">What the work actually costs<a class="gn-heading-anchor" href="#what-the-work-actually-costs" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>That second question — can you actually afford to run it — is the one nobody seems to blog about, probably because almost nobody thinks to measure it. So I did. My day-to-day with Claude Code is attended — I run it on my own machine, with me in the chair. But the number I wanted was the other one: what the same work would cost <em>metered</em>, pay-per-token, the way you&rsquo;d have to pay to run it in the cloud without me babysitting it. That&rsquo;s the expensive lane, and it&rsquo;s the one that decides whether unattended is even on the table. So for a month I logged the metered cost of every real task, next to an honest estimate of how long it would have taken me by hand.</p>
<p>Two hundred and twenty tasks. <strong>About $1,360 in metered cost — and, by my own estimate, close to 540 hours of equivalent by-hand work.</strong> Five hundred and forty hours is the better part of three months of full-time engineering. It went out the door in one.</p>
<p>Put any reasonable number on an engineer-hour and that metered $1,360 stands against work worth <strong>twenty to forty times</strong> as much — around thirty at a normal loaded rate, counting salary plus overhead rather than take-home. I don&rsquo;t need the multiplier to be exact: halve my hour estimates if you think they&rsquo;re generous and it&rsquo;s still an order of magnitude.</p>
<p>The shape of the spend matters as much as the size. The median task cost <strong>$2.45</strong>. Two-thirds of everything came in under five dollars — bug fixes, small features, flaky-CI config, dependency bumps. The expensive runs are rare and they&rsquo;re the meaty ones: a full feature, built end to end, lands in the tens of dollars, and the biggest single rows are features in the <strong>$40–55</strong> range. Stack several rounds of review on one of those and a complete change lifecycle can clear $100. Those are the outliers, not the norm.</p>
<p>The part that decides where to start: I tagged the rows that are pure review-and-fix work — addressing PR comments, fixing a failing build, resolving conflicts. <strong>That whole category was under a tenth of the total spend</strong> — most tasks a few cents to a couple of dollars, half of them under a dollar. And it rarely needed much from me. The comments are bounded, the fix is usually local, and the verification is already wired: the build is green or it isn&rsquo;t. It&rsquo;s the most checkable, lowest-judgment part of the cycle, which makes it the obvious first thing to let run on its own.</p>
<p>The real money leak isn&rsquo;t the price of any one task. It&rsquo;s churn — runs that burned effort on something that didn&rsquo;t need it. A change that should have cost twenty dollars cost a hundred because I let it grind on a problem I should have just steered it through. That waste, not the per-task price, is the thing worth engineering against.</p>
<h2 id="the-scary-number-is-mostly-a-metering-artifact">The scary number is mostly a metering artifact<a class="gn-heading-anchor" href="#the-scary-number-is-mostly-a-metering-artifact" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>There&rsquo;s a version of this where someone sees that monthly total and recoils. &ldquo;You spent <em>how much</em> on an AI?&rdquo; And I get it — $1,360 a month is a genuinely alarming figure.</p>
<p>But it&rsquo;s mostly an artifact of how it&rsquo;s counted, not of what the work is worth — and it isn&rsquo;t even what I pay. The $1,360 is the metered figure I measured. My actual bill is the flat subscription I run this on: a mid-tier &ldquo;Max&rdquo; plan, used at a normal pace — not a hammer-everything-through-the-model, let-me-Google-that-with-Claude-too pace, just steady real work — about €110 a month, call it $120. Same keystrokes, same output, same month of work; the metered figure is roughly ten times the subscription one, purely because of how it&rsquo;s billed.</p>
<p>And here&rsquo;s the bit that quietly defuses the whole cost panic: you work <em>attended</em> anyway. You&rsquo;re sitting there. You&rsquo;re steering. The expensive, unbounded, &ldquo;oh god what is it doing&rdquo; scenario is the <strong>unattended</strong> one — the agent grinding away on its own while the meter spins. Which is exactly the thing you&rsquo;re least ready to trust in the first place.</p>
<p>So the two big worries — &ldquo;it&rsquo;s too expensive&rdquo; and &ldquo;I can&rsquo;t let it run on its own yet&rdquo; — turn out to be the same frontier seen from two sides.</p>
<h2 id="so-what-do-you-actually-build">So what do you actually build?<a class="gn-heading-anchor" href="#so-what-do-you-actually-build" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Not an autonomous loop — not yet, and not for most of it. What I build instead is a <em>human-triggered chain of skills</em>, where &ldquo;skill&rdquo; just means a named, reusable task you can hand the agent, like a saved command. One of mine babysits pull requests: after I open a PR I kick it off, and it watches for the things that need a response — a new round of review comments, a failing build — and works them until the PR is green or it hits something it can&rsquo;t handle and taps me on the shoulder. I start it; it doesn&rsquo;t start itself.</p>
<p>The interesting part is what <em>doesn&rsquo;t</em> run the model. The watching is a plain shell loop polling GitHub every few minutes for a handful of cheap signals — open unresolved comment threads, the CI status, whether the PR&rsquo;s been approved — and it only wakes the agent when that set actually changes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># the watcher is dumb and free — no model in this loop</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> pr_is_open<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="nv">signals</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>unresolved_threads<span class="k">)</span><span class="s2">::</span><span class="k">$(</span>failing_ci<span class="k">)</span><span class="s2">&#34;</span>   <span class="c1"># cheap GitHub calls</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$last</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">        wake_agent <span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span>     <span class="c1"># the only line that spends tokens</span>
</span></span><span class="line"><span class="cl">    <span class="k">fi</span>
</span></span><span class="line"><span class="cl">    <span class="nv">last</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$signals</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    sleep <span class="m">600</span>                     <span class="c1"># 10 min; an idle PR costs nothing</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span></code></pre></div><p>The agent only spins up when that set changes, and even then it does the bounded thing — read the new threads, fix the worthwhile ones in the working tree, tell a &ldquo;my code is broken&rdquo; build apart from an &ldquo;infra fell over&rdquo; one — then goes back to sleep. The expensive part, the model, sits gated behind the cheap part, a script that knows how to read a status. That gating is most of the trick to keeping these things affordable.</p>
<p>Mine is the pragmatic version, though, not the gospel one. Point it at someone else&rsquo;s repo and it needs real guardrails. A PR comment is untrusted input, which turns an open watcher into a prompt-injection surface: someone can leave a comment steering the agent toward something that has nothing to do with the PR, or just spam comments to keep waking it and quietly run up the bill. Past the toy stage you&rsquo;d want a classifier deciding what&rsquo;s even worth acting on, and an escalation path — an OS notification, a Slack ping, a ticket — for the things it shouldn&rsquo;t touch on its own. But you can get a surprising distance on the raw signals alone; &ldquo;open, unresolved comments plus CI status&rdquo; is already enough to be genuinely useful.</p>
<p>And the shape generalizes. Another one I keep meaning to finish: a weekly job that scans the open Dependabot and Renovate pull requests, merges and tidies the boring ones where nothing real has to change, and escalates the rest — the bumps that need an actual refactor, or where an API might break under you. Same skeleton every time: a cheap trigger, a bounded job, and a clear line where it hands back to a human. Building those loops, and the small toolkits around them, is quietly becoming its own kind of software engineering — less &ldquo;prompt the model,&rdquo; more &ldquo;design the harness the model runs inside.&rdquo;</p>
<p>Two rules I&rsquo;d carve in stone before building any of it. First: <strong>release is not a job for the model — it&rsquo;s your CI/CD.</strong> Don&rsquo;t teach an agent to do the thing your pipeline already does for free, deterministically, with an audit trail; that&rsquo;s not autonomy; it&rsquo;s reinventing a solved problem. Second: <strong>monitoring is not a job for the model — it&rsquo;s Sentry and Grafana.</strong> Don&rsquo;t pay a brain to watch a dashboard; wire the alert back to a trigger so the agent gets handed the problem only when something has actually broken. And whatever you build, cap the fix loop — a hard iteration limit plus a human &ldquo;good enough, ship it&rdquo; gate — or that $20 fix quietly becomes a $100 one while you&rsquo;re at lunch.</p>
<p>None of this is timidity. Human-in-the-loop is simply how you <em>start</em>: you loosen the leash one iteration at a time, as your own skills sharpen and, more importantly, as your verification gets good enough that you&rsquo;d trust the result without standing over it.</p>
<h2 id="weve-had-this-exact-argument-before">We&rsquo;ve had this exact argument before<a class="gn-heading-anchor" href="#weve-had-this-exact-argument-before" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>If all of this feels familiar, it&rsquo;s because we ran the entire play a decade ago. It was called microservices, and I was there for it. Quick recap if you missed that era: around 2014, teams started chopping big single applications — &ldquo;monoliths&rdquo; — into lots of small, independently deployable services. The loud argument was how <em>small</em> they should be.</p>
<p>The froth is identical. Back then the unanswerable question was &ldquo;how small is a microservice — can it be 200 lines?&rdquo; Today it&rsquo;s &ldquo;Ralph or <code>/goal</code>, and how many agents?&rdquo; In both cases it&rsquo;s a community sizing-debating its way through a thing before anyone yet knows what actually matters.</p>
<p>The good practice predated the name, too. We were splitting systems by domain — clean seams, owned boundaries — long before anyone said &ldquo;microservices.&rdquo; And the agent loop plus a verification harness existed long before anyone branded it &ldquo;loop engineering.&rdquo; The name showed up and took credit for work people were already doing.</p>
<p>And then it settled. The honest retrospective on microservices isn&rsquo;t &ldquo;we revolutionized everything.&rdquo; It&rsquo;s &ldquo;we mostly kept building the way we already built, with cleaner seams.&rdquo; Most teams went from a monolith to two or five services, not one to two hundred overnight. The change was real and it was undramatic, because most of what we built was already small.</p>
<p>But here&rsquo;s the correction that actually matters, the part the sizing-debate missed at the time. For microservices, the real delta was never the pattern. It was the <strong>substrate.</strong> Containers, then the cloud, then Kubernetes made small, independently deployable services cheap and standard to run. The pattern was downstream of the infrastructure — and the infrastructure showed up to meet it.</p>
<p>For loops, we&rsquo;re only halfway there. The capability substrate has arrived, and it&rsquo;s the <strong>model.</strong> One row on my sheet is a feature I&rsquo;d have budgeted two days of hand-work for, delivered for about fifty dollars. That was flatly impossible two years ago, at any price, with any loop. The loop didn&rsquo;t change; the thing inside it got good enough to trust with real work. That&rsquo;s the containers moment — the raw capability landing.</p>
<p>What hasn&rsquo;t arrived is the rest of the stack. There&rsquo;s no cheap, standard, safe place to actually run autonomy unattended — no equivalent of the managed cloud and Kubernetes that turned &ldquo;I have a small service&rdquo; into &ldquo;and here&rsquo;s the obvious, affordable place to run it.&rdquo; Nothing yet says &ldquo;let this agent grind on its own overnight without it costing a fortune or going off the rails.&rdquo;</p>
<p>So: the model is the containers; the Kubernetes hasn&rsquo;t been built yet. And &ldquo;loops&rdquo; — the philosophy, the sizing wars — is the <em>nanoservices</em> froth all over again: the &ldquo;how absurdly small can we make it?&rdquo; overreach, playing out on a stack that&rsquo;s still missing its bottom half.</p>
<h2 id="autonomy-is-granular-not-a-switch">Autonomy is granular, not a switch<a class="gn-heading-anchor" href="#autonomy-is-granular-not-a-switch" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Which brings me back to where I started. You don&rsquo;t flip from &ldquo;I watch everything&rdquo; to &ldquo;it runs itself.&rdquo; Nobody went from one service to two hundred in a weekend, and nobody sane is going from attended-everything to autonomous-everything either.</p>
<p>The leash comes off one verifiable task-class at a time, gated by exactly one thing: how checkable the verification is. It&rsquo;s already happening. A PR-watcher that auto-fixes failed builds is the leash already off — on a narrow, bounded, machine-checkable class where the cost of being wrong is low and a green check tells you the truth. Meanwhile feature design and production changes stay leashed for a long time, maybe forever, because the verification is judgment and judgment doesn&rsquo;t fit in a CI check.</p>
<p>So when you read &ldquo;I design loops, not prompts,&rdquo; understand where it&rsquo;s coming from. It&rsquo;s not a lie. But notice who tends to say it: people at the big AI labs — the ones building these models — or with effectively unlimited token budgets, or both. For them the missing substrate isn&rsquo;t missing; the meter doesn&rsquo;t apply the way it applies to you. &ldquo;Just run loops&rdquo; is a perfectly honest description of <em>their</em> reality. It&rsquo;s frontier-reporting: true where it&rsquo;s reported from, with the best tooling and the most checkable problems, sold as a universal prescription it isn&rsquo;t yet for anyone working against a real budget.</p>
<p>So I&rsquo;m not tuning out the debate — I&rsquo;m tuning out the word. I build the attended chain, harden my own skills and verification, and let each task-class earn its slack one check at a time. That&rsquo;s the part I can act on today, while the rest gets argued out.</p>
<p>Which is the same move that turned out right with microservices: clock the froth, let the sizing wars run, and keep shipping the boring version that actually works.</p>
]]></content:encoded>
      <category>AI</category>
    </item>
    <item>
      <title>A clean machine</title>
      <link>https://build.golfnext.com/en/posts/clean-machine/</link>
      <pubDate>Tue, 23 Jun 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/clean-machine/</guid>
      <description>Two years after a colleague&#39;s dotfiles scrolled past in my GitHub feed, a new laptop finally forced the issue. How I rebuilt my Mac as a repo with chezmoi, ran my once-a-year tool review as a conversation with Claude, and automated the parts I used to click through.</description>
      <content:encoded><![CDATA[<p>In the last post I set myself a goal for the first ninety days: a five-minute cold start. Fresh laptop to running code in under five minutes — clone the project, run it locally, hit a live endpoint, pass the tests, all before your coffee goes cold. The shortest honest path from a new hire&rsquo;s first morning to their first real contribution. That&rsquo;s the on-ramp I want to build for the team at GolfNext, the Danish golf-tech company I&rsquo;m about to join.</p>
<p>Before I can hand that to anyone else, I owe it to my own machine. A new job means a new MacBook is on the way, so I pointed the same goal at myself first.</p>
<h2 id="the-two-year-old-bookmark">The two-year-old bookmark<a class="gn-heading-anchor" href="#the-two-year-old-bookmark" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>About two years ago, a <a href="https://github.com/brandstetterm/dotfiles">dotfiles repo</a> rolled past in my GitHub timeline — the feed of what the people you follow are working on. It belonged to Manuel Brandstetter, a former colleague and one of the best engineers I&rsquo;ve worked alongside. He took over as lead of <a href="https://scrumlr.io">scrumlr.io</a>, the open-source retrospective tool I helped build, after I left inovex, the German consultancy where we&rsquo;d worked together. So when his setup scrolled by, I stopped to look. No grand pitch, just there in passing: everything that makes a laptop <em>his</em> laptop, version-controlled in one place. I remember thinking, clearly and out loud, &ldquo;I should do that.&rdquo; Then I didn&rsquo;t. For two years.</p>
<p>If you&rsquo;ve never kept dotfiles: they&rsquo;re the small config files that quietly shape your machine. Your shell setup, your editor settings, your git config — the dozens of little preferences you&rsquo;d otherwise rebuild by hand, from memory, every time you get a new computer. A dotfiles repo puts them under version control so the machine becomes reproducible instead of hand-crafted and forgotten.</p>
<p>The reason I never got around to it is the boring one: it always felt like an afternoon I could spend on something more urgent. The thing about a brand-new laptop, though, is that it removes the excuse. You&rsquo;re going to set the machine up anyway. You may as well set it up <em>once</em>, properly, in a way you never have to repeat.</p>
<h2 id="the-audit-i-run-every-december">The audit I run every December<a class="gn-heading-anchor" href="#the-audit-i-run-every-december" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>There&rsquo;s a habit I&rsquo;ve kept for years. Somewhere in the quiet week around Christmas, I sit down and audit my tools. Am I still using the best thing for each job, or just the thing I happened to pick three years ago and never questioned? It&rsquo;s usually a solo afternoon — reading changelogs, half-remembered Hacker News threads, a bit of &ldquo;what&rsquo;s everyone else using now.&rdquo;</p>
<p>This year had a running start. A few weeks earlier I&rsquo;d gone looking for something like nvm, but for <em>every</em> runtime, not just Node: Python, Go, Java, the lot. I found <a href="https://mise.jdx.dev">mise</a>, which does exactly that, and swapping a drawer full of per-language version managers for a single one was the most satisfying thing I&rsquo;d done to my setup in months. That small win, I suspect, is what quietly tipped a two-year-old &ldquo;I should do that&rdquo; into &ldquo;do it now.&rdquo;</p>
<p>So this year I ran the audit as a conversation instead. I pointed Claude — Anthropic&rsquo;s AI assistant, the same one I&rsquo;ll be building with at work — at my current setup and asked the blunt version of the question: what here is dated, what&rsquo;s missing, what would you change, and why. Then I let it research and argue its case.</p>
<p>I kept the result honest by making it a diff. The old setup stayed on my <code>main</code> branch; the reviewed one went on a branch I called <code>cleanup</code>. So every recommendation was a line I could read, accept, or reject, not a black box rewriting my machine while I wasn&rsquo;t looking. That framing mattered more than I expected. A review you can read line by line is a review you actually trust.</p>
<p>Two things came out of it: a way to treat the whole machine as one repo, and a short list of tools I&rsquo;d been silly to skip.</p>
<h2 id="chezmoi-the-machine-as-a-repo">chezmoi: the machine as a repo<a class="gn-heading-anchor" href="#chezmoi-the-machine-as-a-repo" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The manager is <a href="https://chezmoi.io">chezmoi</a>. It tracks your dotfiles in a git repo and renders them onto any machine you own — <em>chez moi</em>, fittingly, French for &ldquo;at my place.&rdquo;</p>
<p>What sold me over a plain folder of symlinks is the model. The files in the repo are the source of truth; you <code>apply</code> them to write the real files into your home directory. Edit, preview the diff, apply, commit. It templates (one file can adapt to a work machine versus a personal one), and it keeps secrets out of the repo entirely — my SSH keys stay inside <a href="https://developer.1password.com/docs/ssh/">1Password</a>, handed to git on demand, never written to a file chezmoi could accidentally commit.</p>
<p>The part that finally delivered on the two-year promise is the bootstrap. On a clean Mac, the whole machine comes back from three commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">brew install gh chezmoi
</span></span><span class="line"><span class="cl">gh auth login                              <span class="c1"># sign in to GitHub in the browser</span>
</span></span><span class="line"><span class="cl">chezmoi init --apply bitionaire/dotfiles   <span class="c1"># clone + render everything into place</span>
</span></span></code></pre></div><p>That last line clones the repo, installs my shell framework, lays down every config file, and runs a set of first-time setup scripts. One naming convention does most of the magic: a file called <code>dot_zshrc</code> in the repo becomes <code>~/.zshrc</code> on the machine. <code>dot_config/ghostty/config</code> becomes <code>~/.config/ghostty/config</code>. The <code>dot_</code> prefix is just chezmoi&rsquo;s way of writing a leading dot without confusing your file browser. Once that clicks, the repo reads like a mirror of your home directory.</p>
<h2 id="the-tools-the-review-actually-changed">The tools the review actually changed<a class="gn-heading-anchor" href="#the-tools-the-review-actually-changed" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I already ran <a href="https://ohmyz.sh">oh-my-zsh</a> with the <a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a> prompt. The review pushed three additions hard, and was right about all three:</p>
<ul>
<li><strong><a href="https://ghostty.org">Ghostty</a></strong> — a fast, modern terminal. I&rsquo;d been on the macOS default for years out of pure inertia. Ghostty is quicker, themeable in a plain text file (which means chezmoi can track it), and renders the powerline glyphs my prompt uses without a separate Nerd Font install.</li>
<li><strong><a href="https://github.com/junegunn/fzf">fzf</a></strong> — a fuzzy finder, and the one that changed my day-to-day most. <code>Ctrl-R</code> turns shell history from a blind up-arrow hunt into a live fuzzy search. <code>Ctrl-T</code> drops a file path into whatever you&rsquo;re typing. Pipe anything into it — <code>git branch | fzf</code> — and you get an interactive picker for free.</li>
<li><strong><a href="https://github.com/ajeetdsouza/zoxide">zoxide</a></strong> — a smarter <code>cd</code>. It learns the directories you actually use and lets you jump by a fragment of the name. <code>z dot</code> lands me in my dotfiles repo from anywhere on the machine, no matter how deep it&rsquo;s buried.</li>
</ul>
<p>None of these are exotic — they&rsquo;re tools a lot of terminal-heavy people already swear by, and I&rsquo;d simply never sat down to add them. Wiring them in is a few lines of shell config:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># fzf — fuzzy finder (Ctrl-R history search, Ctrl-T file picker)</span>
</span></span><span class="line"><span class="cl"><span class="nb">command</span> -v fzf &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">&amp;&amp;</span> <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>fzf --zsh<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># zoxide — smarter cd (`z &lt;dir&gt;` jumps by frequency + recency)</span>
</span></span><span class="line"><span class="cl"><span class="nb">command</span> -v zoxide &gt;/dev/null 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="o">&amp;&amp;</span> <span class="nb">eval</span> <span class="s2">&#34;</span><span class="k">$(</span>zoxide init zsh<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><h2 id="automating-the-parts-i-used-to-click-through">Automating the parts I used to click through<a class="gn-heading-anchor" href="#automating-the-parts-i-used-to-click-through" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>A fresh Mac isn&rsquo;t just dotfiles. It&rsquo;s an afternoon of downloading apps, dragging them to Applications, and clicking through the same System Settings toggles you&rsquo;ve set on every Mac you&rsquo;ve ever owned. That&rsquo;s the part the review automated, and it&rsquo;s the part I&rsquo;m proudest of.</p>
<p>Apps come from a single <code>Brewfile</code> — a manifest <a href="https://brew.sh">Homebrew</a> reads to install everything in one shot:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">brew bundle --file<span class="o">=</span>Brewfile   <span class="c1"># installs every CLI tool, app, and App Store entry</span>
</span></span></code></pre></div><p>One file lists the command-line tools, the desktop apps (1Password, Firefox, Ghostty, Docker, the editor), and even Mac App Store purchases. New machine, one command, twenty-odd apps install themselves.</p>
<p>Then there are the macOS settings. chezmoi runs setup scripts on first apply, and one of them is just a list of <code>defaults write</code> commands — the scriptable side of System Settings. A taste:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">defaults write -g KeyRepeat -int <span class="m">2</span>                            <span class="c1"># fast key repeat</span>
</span></span><span class="line"><span class="cl">defaults write -g ApplePressAndHoldEnabled -bool <span class="nb">false</span>        <span class="c1"># hold a key to repeat it</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.finder AppleShowAllFiles -bool <span class="nb">true</span>  <span class="c1"># show hidden files</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.finder _FXSortFoldersFirst -bool <span class="nb">true</span>  <span class="c1"># folders on top</span>
</span></span></code></pre></div><p>I&rsquo;d set every one of these by hand on every Mac for a decade and never thought to write them down — now they&rsquo;re a file. It isn&rsquo;t total. Apple deliberately blocks scripting the security-sensitive bits, so granting accessibility permissions and signing into iCloud stays manual. But the dozens of little toggles that used to eat an afternoon now take seconds, and I&rsquo;ll never have to remember them again.</p>
<h2 id="a-good-review-cuts-as-much-as-it-adds">A good review cuts as much as it adds<a class="gn-heading-anchor" href="#a-good-review-cuts-as-much-as-it-adds" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The surprise was how much the conversation talked me <em>out</em> of. A tool audit isn&rsquo;t only about new toys; it&rsquo;s about noticing the cruft you&rsquo;ve been carrying.</p>
<p>So the <code>cleanup</code> branch deleted things. The handful of older version managers I&rsquo;d accumulated, plus a hand-installed Python, all folded into the one mise I&rsquo;d adopted a few weeks before. A shortcut I&rsquo;d wired up to work around an old Docker quirk that&rsquo;s since been fixed — gone. A tangle of work-versus-personal git config I&rsquo;d accreted across jobs, flattened back to a single identity. Each of those was a small decision I&rsquo;d made years ago for a good reason, then never revisited once the reason expired.</p>
<p>That&rsquo;s the real value of doing this with a sparring partner. Left alone, I tend to add. Asked to justify each piece out loud, I finally removed.</p>
<h2 id="what-im-taking-from-it">What I&rsquo;m taking from it<a class="gn-heading-anchor" href="#what-im-taking-from-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The impressive part wasn&rsquo;t that an AI can write a config file — everyone knows it can now. It was the review. It didn&rsquo;t hand me a generic &ldquo;best setup&rdquo;; it looked at <em>mine</em>, asked what I actually do all day, and argued for changes that fit, with the reasoning attached so I could push back where it was wrong. Less a tool than the December audit I already run, with someone knowledgeable sitting across the table.</p>
<p>And it closed a loop. The five-minute cold start I want to give the GolfNext team is this same thing, scaled up: a new person, a new laptop, and a short path to real work without a week of setup tax. I built the personal version first.</p>
<p>So if you&rsquo;ve got your own &ldquo;I should do that&rdquo; bookmark gathering dust — a repo that once scrolled past in your timeline, maybe — this is your sign. Wait for the next clean machine. Then set it up once. Two years late still beats never.</p>
<h2 id="references">References<a class="gn-heading-anchor" href="#references" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The tools and docs worth bookmarking, roughly in the order they show up above:</p>
<ul>
<li><strong><a href="https://chezmoi.io">chezmoi</a></strong> — dotfiles manager. Start with the <a href="https://www.chezmoi.io/quick-start/">quick start</a> and the <a href="https://www.chezmoi.io/user-guide/setup/">user guide</a>.</li>
<li><strong><a href="https://brew.sh">Homebrew</a></strong> and the <strong><a href="https://github.com/Homebrew/homebrew-bundle">Brewfile / <code>brew bundle</code></a></strong> — install apps and CLI tools from one manifest.</li>
<li><strong><a href="https://ohmyz.sh">oh-my-zsh</a></strong> — Z shell framework, with the <strong><a href="https://github.com/romkatv/powerlevel10k">powerlevel10k</a></strong> prompt theme.</li>
<li><strong><a href="https://ghostty.org">Ghostty</a></strong> — fast, config-as-text terminal.</li>
<li><strong><a href="https://github.com/junegunn/fzf">fzf</a></strong> — command-line fuzzy finder.</li>
<li><strong><a href="https://github.com/ajeetdsouza/zoxide">zoxide</a></strong> — a smarter <code>cd</code> that learns your habits.</li>
<li><strong><a href="https://mise.jdx.dev">mise</a></strong> — one version manager for node, python, go, and the rest.</li>
<li><strong><a href="https://developer.1password.com/docs/ssh/">1Password SSH agent</a></strong> — keep private keys out of the repo and off disk.</li>
<li>The repo that started it: <strong><a href="https://github.com/brandstetterm/dotfiles">Manuel Brandstetter&rsquo;s dotfiles</a></strong>.</li>
</ul>
<h2 id="ps--a-dock-trick-that-has-nothing-to-do-with-dotfiles">P.S. — a Dock trick that has nothing to do with dotfiles<a class="gn-heading-anchor" href="#ps--a-dock-trick-that-has-nothing-to-do-with-dotfiles" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>One freebie for the Mac users who made it this far. It has nothing to do with any of the above, but it&rsquo;s the kind of small thing that&rsquo;s quietly pleased me for years: you can drop blank spacers into the Dock to break your apps into clusters. Same <code>defaults write</code> trick as the rest of the post. Run it once per spacer you want:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="c1"># full-height spacer (run again for each extra one)</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.dock persistent-apps -array-add <span class="s1">&#39;{tile-data={}; tile-type=&#34;spacer-tile&#34;;}&#39;</span> <span class="o">&amp;&amp;</span> killall Dock
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># half-height spacer (run again for each extra one)</span>
</span></span><span class="line"><span class="cl">defaults write com.apple.dock persistent-apps -array-add <span class="s1">&#39;{&#34;tile-type&#34;=&#34;small-spacer-tile&#34;;}&#39;</span> <span class="o">&amp;&amp;</span> killall Dock
</span></span></code></pre></div><p>Drag the spacers around like any other Dock icon, or drag one off the Dock to remove it. Tiny thing. Makes a crowded Dock feel organized.</p>
]]></content:encoded>
      <category>Engineering</category>
      <category>Infrastructure</category>
    </item>
    <item>
      <title>A letter to myself, before day one</title>
      <link>https://build.golfnext.com/en/posts/letter-day-one/</link>
      <pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><dc:creator>Johann Böhler</dc:creator>
      <guid>https://build.golfnext.com/en/posts/letter-day-one/</guid>
      <description>Before my first day at GolfNext — and before I&#39;ve even signed — what I&#39;m walking into, why I&#39;m leaving a job I liked, the goals I&#39;m setting myself for the first ninety days, and the compass I&#39;m bringing with me.</description>
      <content:encoded><![CDATA[<p><em>&ldquo;I may have something of interest to you, Johann. Would be great to connect 👍🏼.&rdquo;</em></p>
<p>That&rsquo;s the whole message. It arrived on LinkedIn out of nowhere, from a recruiter named Joe — which, given how often these things come in under a borrowed first name, might even be his real one. No company, no role, no details. The kind of message you&rsquo;d normally archive without a second thought.</p>
<p>This one ends with me leaving a job I like, at a company I have nothing bad to say about, for a company whose name Joe wouldn&rsquo;t even tell me. I had to find that out myself. Well — myself, and an AI.</p>
<p>I&rsquo;m writing this before my first day of work. I haven&rsquo;t signed the contract yet; I don&rsquo;t even know my start date. Writing the company blog before I officially have the job sounds backwards. Maybe it is. I&rsquo;m doing it anyway, because I&rsquo;m carrying around more thoughts than I can hold, and because I want a record — something to read back in six months and hold up against whatever actually happened.</p>
<p>So this is two things at once. The story of how a two-line message turned into a job, and a letter to a future version of me.</p>
<p>A confession before we start: I&rsquo;ve never written a blog post in my life. I&rsquo;m starting now on purpose — writing pins down ideas that are otherwise just vapor. So here I am, thinking out loud.</p>
<h2 id="from-three-breadcrumbs-to-a-company">From three breadcrumbs to a company<a class="gn-heading-anchor" href="#from-three-breadcrumbs-to-a-company" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>I asked Joe what his message was about. It turned out to be a Lead Engineer role at a small but established company: the first cornerstone of an AI-first development team — one that builds software mostly by directing AI instead of typing every line by hand. Joe wouldn&rsquo;t name it. He gave me breadcrumbs instead — Danish sports tech, around since 2012, 400+ venues across 18 countries.</p>
<p>So I handed the breadcrumbs to Claude, Anthropic&rsquo;s AI assistant. In seconds it named the company: GolfNext, in Ry, a short drive from where I live in Denmark. They make the self-service kit golf clubs run on — more on that later.</p>
<p>That little exercise is more or less the job description. If you can point an AI at three vague clues and have it reconstruct the company behind them, that&rsquo;s the work. So I replied with a fully AI-generated message, openly labeled:</p>
<p><em>&ldquo;Full disclosure, Joe: this reply is AI-generated. Felt fitting for an AI-first pitch — and yes, I let the agent do the detective work too. 😄&rdquo;</em></p>
<p>They wrote back. A few days later I drove out to Ry and spent an hour at the GolfNext office, and by the time I left I knew I wanted it. The tell was the first thing they said when I sat down: <em>&ldquo;Let&rsquo;s just have a chat here and talk openly.&rdquo;</em> No theater.</p>
<p>Before committing, I wanted to know exactly what I&rsquo;d be walking into, so I asked. Thomas Herskind, who runs product at GolfNext, sent over an overview of the systems they run today. I read it and wrote back with everything I thought was worth doing first, point by point. It ran long. Very long.</p>
<p>Thomas&rsquo;s reply was six words. <em>&ldquo;Congrats on your first novel, Johann!&rdquo;</em></p>
<p>I laughed out loud. You learn a lot about a place from how it teases you before you&rsquo;ve even signed.</p>
<h2 id="the-70-ideas-before-this-one">The 70 ideas before this one<a class="gn-heading-anchor" href="#the-70-ideas-before-this-one" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>None of which explains why a two-line message with no details got my attention in the first place. For that, I have to rewind a few months — and first tell you who&rsquo;s writing, since this is the internet.</p>
<p>I&rsquo;m a generalist. A full-stack engineer who never minded picking up whatever a project happened to need. Sixteen years in: a lot of backend, a lot of web apps, a few years leading a team, and co-creator of scrumlr, an open-source tool teams use to run retrospectives — those meetings where you look back at what went well and what didn&rsquo;t. I&rsquo;ve never been the engineer who loves technology for its own sake. What pulls me is the other end of the wire: the user, the design, the value, the thing that makes somebody&rsquo;s day a little easier. And getting there without a lot of detours. That&rsquo;s the CV in a paragraph. The itch is the part it can&rsquo;t carry.</p>
<p>I&rsquo;d had an itch for a while to build something of my own. Anything that might grow into a business, really — and an excuse to push Claude as far as it would go and find out how much real software it could actually build. The first project came out of a pet peeve I&rsquo;d carried across jobs. Release notes — the &ldquo;what changed&rdquo; list that&rsquo;s supposed to be for the people using the software — keep getting dumped into commit messages, the terse notes developers leave each other. <em>&ldquo;Upgrade ESLint to version 10.&rdquo;</em> Who is that for? So I built a small product that lets you write proper change entries in plain language, right next to the code, and hand them to the product managers who should own them. Built from scratch in about two weeks — the parts users see, the backend behind them, the pipeline that ships updates on its own. All of it, with AI doing most of the typing.</p>
<p>That felt good enough that I got greedy and reached for something much bigger: an agent that keeps software projects alive. Maintaining them, extending them, mostly on its own, with the release-notes tool as its first patient. That&rsquo;s where I hit a wall, and the wall taught me more than most of my finished projects did.</p>
<p>Some of it was scope. I&rsquo;d wrapped a full product around something that should have started as a small experiment. But the deeper lesson was about the tool. Today&rsquo;s AI is genuinely brilliant at a blank page, and at small, well-bounded changes. It gets shaky the moment you ask it to tear into big chunks of core logic. It starts to hallucinate — confidently inventing things that aren&rsquo;t there. It never quite cleans up after itself. Old context lingers and creeps back into the next attempt. A build that ambitious needs a clear plan for <em>how</em> you&rsquo;ll get there before you write a single line, and holding that plan together on my own, in stolen evenings and weekends, just wasn&rsquo;t something I could pull off. That project is parked, not buried.</p>
<p>So I changed tactics. I put Claude to work as a sparring partner on smaller things, the kind I could finish in a couple of weeks. I&rsquo;d pitch an idea; it would go research the market and, more often than not, come back with the same answer — crowded, already funded, don&rsquo;t bother. Painful. Also useful.</p>
<p>After about 70 of those, I stopped pitching and asked the obvious question: so what <em>should</em> I do? The answer was refreshingly blunt. <em>Stop hunting for a clever product. Find someone with a real problem and help them build a software-driven process around it that saves them serious time.</em> And since it knows how I work, it sketched out roughly what that would look like. A few months later, Joe&rsquo;s two lines landed in my inbox — and the role behind them, once I asked, turned out to be a near-perfect match for that sketch. The message with no details was the thing I&rsquo;d been told to go find.</p>
<h2 id="what-its-actually-about">What it&rsquo;s actually about<a class="gn-heading-anchor" href="#what-its-actually-about" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>High time I told you what I&rsquo;d actually found. GolfNext builds self-service tools and hardware for golf clubs — picture a ball dispenser members work themselves at the range, or a system that takes the fiddly, error-prone business of selling and redeeming vouchers and makes it calm. For years, capable external partners built most of that software, and it carried the company a long way. The shift now, as they laid it out to me, is that GolfNext wants the engineering know-how to live inside the company: a team that understands the whole landscape from the inside, for the kind of steady, long-term growth that comes from owning what you depend on. No verdict on the people who built it so far — just the natural next step for a company that plans to be around for a long time.</p>
<p>I&rsquo;m one of the first pieces of that. The brief: absorb the landscape — the same one Thomas previewed in that overview — take over development bit by bit, and before long help build a team around it.</p>
<p>Here&rsquo;s a confession that sounds odd in an engineering blog: I&rsquo;ve barely written code by hand in months. Call it half a year on my own projects, a couple of months at work. Not because I stopped building. The building just moved up a level. More of my day now goes into directing AI, reviewing what it gives back, and designing the guardrails that keep its output safe and correct. It&rsquo;s where the craft is heading — and it&rsquo;s exactly the team GolfNext is asking me to build. Nobody can honestly tell you where all this lands; the ground moves every few weeks. But the direction is clear enough to point at, and pointing at it is the job.</p>
<p>That&rsquo;s the <em>what</em>. Before the <em>how</em>, there&rsquo;s a goodbye I owe.</p>
<h2 id="leaving-a-good-thing">Leaving a good thing<a class="gn-heading-anchor" href="#leaving-a-good-thing" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>First, the honest part nobody enjoys writing. Saying yes here means leaving Salling Group — one of Denmark&rsquo;s largest retailers — and I don&rsquo;t want a single word of this read as a complaint. Salling was the perfect first step when I landed in Denmark: a real culture, people who genuinely look out for each other, work I&rsquo;m proud of. I shipped things that mattered, under deadlines with no room for error, next to engineers I learned plenty from. I made good friends there. Part of me is sad to go.</p>
<p>The reason I&rsquo;m moving isn&rsquo;t a flaw in the place. It&rsquo;s a difference in what I&rsquo;m built for. At a company that size, every decision gets weighed by a lot of thoughtful people, which is exactly how a big retailer earns and keeps people&rsquo;s trust. But it means being close to the wheel is rare, and steering quickly rarer still. I want the wheel. I want to point at a direction and move. That itch is most of why I started building things on the side in the first place, and it&rsquo;s precisely what this new role hands me. Leaving a good thing for the right reason is still leaving a good thing. Both are true at once.</p>
<h2 id="the-goals-im-setting-for-myself">The goals I&rsquo;m setting for myself<a class="gn-heading-anchor" href="#the-goals-im-setting-for-myself" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>Now for the <em>how</em>. The wall I hit on that side project is the whole reason the goals below look the way they do. If AI is strongest inside clear boundaries, the answer isn&rsquo;t to prompt harder. It&rsquo;s to build a landscape where the boundaries are obvious — where context is cheap to hand over, where a change is easy to scope small, where the guardrails catch what the model gets wrong before a human has to. Because that&rsquo;s where I think this is all heading: the scarce resource in software becomes how fast anyone — human or AI — can pick up a context they&rsquo;ve never seen. If a new engineer and a fresh AI session can both understand a service quickly, work can start immediately, by whoever picks it up. If they can&rsquo;t, every task starts with archaeology. Most of what follows is really about that one sentence.</p>
<p>One disclaimer that matters: I haven&rsquo;t started yet. These are the standards I&rsquo;ll hold myself to, not decisions I get to make alone — the order and the priorities get worked out with the people who&rsquo;ve been running these systems for years.</p>
<p><strong>Make the work visible.</strong> This blog is goal one. When you&rsquo;re building knowledge inside a team, the worst thing you can do is lock it away in private channels and individual heads. Writing it down in the open forces clarity, pulls in good people, keeps everyone honest. So it exists now. Future me: if these posts went quiet, take it as a warning sign.</p>
<p><strong>Rebuild the understanding, not just the access.</strong> Taking over from an external team isn&rsquo;t really about collecting credentials and passwords. It&rsquo;s about rebuilding the understanding from the inside. So: every account checked and put in my name, every doc read and sharpened until I can operate and release anything in the stack without phoning a friend. That&rsquo;s the unglamorous half of the ninety days, and it&rsquo;s the half everything else waits on.</p>
<p><strong>Build a landscape everyone can build in.</strong> This is the one I care about most. Concretely:</p>
<ul>
<li><strong>A five-minute cold start.</strong> Fresh laptop to running code: understand the project, run it locally, hit a live endpoint, pass the test suite, all in under five minutes. It&rsquo;s the on-ramp to building and testing, not a rulebook for how to write code. Future me, this one is measurable — go time it.</li>
<li><strong>Docs that live right next to the code</strong>, so context is one directory away instead of one Slack message away.</li>
<li><strong>Automation for anything we&rsquo;d otherwise do by hand twice.</strong></li>
<li><strong>Technical debt paid down where it&rsquo;s genuinely in the way</strong> — not a spring clean, just the things standing in the road.</li>
<li><strong>A shared kit of prompts and presets for the AI</strong>, so everyone starts from the same proven setup instead of rebuilding it each time. That&rsquo;s what turns &ldquo;AI-first&rdquo; into actual shared tooling instead of a slogan.</li>
</ul>
<p>The point isn&rsquo;t to be the only people who can build. It&rsquo;s to be the quality gate that turns a rough idea into something you can trust, and the helping hand that makes building safe and easy for everyone else.</p>
<p><strong>Stay on the keys.</strong> I said this to Joe before I&rsquo;d said it to anyone at GolfNext, because it would save us both time. I&rsquo;ve led a team before, twenty-four people, and I stepped back to building on purpose. <em>&ldquo;If &lsquo;Lead&rsquo; means setting technical direction while still building, I&rsquo;m very in. If it means drifting away from the code, I&rsquo;m the wrong guy.&rdquo;</em> That line started as a filter for the job. It&rsquo;s a goal now. Future me: if you&rsquo;ve drifted, you know what to do about it.</p>
<p><strong>Hire excellent people, and then trust them.</strong> Not in the first ninety days, and not my call alone, but it&rsquo;s where this is headed, and it shapes the early calls. A small team of excellent people needs almost no process. Keep the bar high even when you&rsquo;re short-handed. That part, at least, isn&rsquo;t me projecting my reading list onto them: when hiring came up in Ry, they told me they&rsquo;d rather run short-handed for a few months than bring in the wrong person just to add capacity. It told me more about the place than any pitch could have. Hand people context instead of approvals. Judge the work, not the noise. Default to yes, and stop to debate only at the doors you can&rsquo;t walk back through. And leave room — real room — to spitball, to argue, to build things that might not work. The creative, playful time isn&rsquo;t a reward you earn once the serious work is done. In this line of work, it <em>is</em> the serious work.</p>
<h2 id="a-compass-not-a-rulebook">A compass, not a rulebook<a class="gn-heading-anchor" href="#a-compass-not-a-rulebook" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>The goals are the plan. Underneath the plan sits a compass, and it&rsquo;s worth showing you, because it&rsquo;ll shape how I steer.</p>
<p>What I won&rsquo;t show up with is a manifesto. I don&rsquo;t believe a culture can be written into existence. It grows out of its environment and the people in it, day by day, in a thousand small moments nobody writes down — which is why it can only be led by example, never enforced. We&rsquo;ll work out GolfNext&rsquo;s actual values together, in the doing. And I mean <em>together</em> in both directions. I&rsquo;ll arrive with convictions — this post is full of them — but I expect to be changed as much as I change anything, and to pick up ways of working I haven&rsquo;t even considered yet.</p>
<p>Part of it I owe to inovex, the German consultancy where I spent seven years — the longest stretch of the sixteen. They had a triangle — Employees, Customers, Economy — and they meant every corner of it: fair pay, the freedom to choose the right tools for the job, sane and agile ways of working, genuine care for the people doing the work. Most of that I still hold to. My own emphasis just sits a little differently. Where their triangle puts employee happiness, mine puts <em>Craft</em>. Not because happiness doesn&rsquo;t matter, but because I&rsquo;ve come to think it grows out of doing great work together, rather than being something you can aim at head-on.</p>
<p>So the three points I steer by are <em>Customer</em> (the aim — solving the problem people would genuinely miss), <em>Craft</em> (the swing — building it well, and getting better by building it), and <em>Capital</em> (the long game — staying healthy enough to finish the round, and keeping the knowledge in the house so nothing important lives in a single head). Lean too hard on any one and you slice; get all three swinging together and the ball drops.</p>
<h2 id="what-ive-been-reading-and-what-i-took-from-it">What I&rsquo;ve been reading, and what I took from it<a class="gn-heading-anchor" href="#what-ive-been-reading-and-what-i-took-from-it" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>A fair number of the goals above — the high bar, the trust, the near-absence of process — I got from <em>No Rules Rules</em>, Reed Hastings and Erin Meyer&rsquo;s book on how Netflix is run. The argument, compressed: push talent density up until you have a team of excellent people, use candor to keep them honest with each other, and then start taking the rules away — because most rules exist to stop bad outcomes from average performers, and once you haven&rsquo;t got those, the rules cost more than they save.</p>
<p>The bit I keep coming back to is the expense policy. It comes down to a single instruction: <em>act in the best interest of the company.</em> That looks like a policy about receipts. It isn&rsquo;t. It&rsquo;s a statement about who&rsquo;s responsible. A rulebook hands you an answer somebody else worked out once, for a situation that might not be yours anymore. One instruction hands you the <em>goal</em> and makes you work out the answer for the situation you&rsquo;re actually standing in. That&rsquo;s more weight to carry. It&rsquo;s also what makes you a real part of the thing instead of an executor of other people&rsquo;s decisions — and it&rsquo;s where creativity and initiative come from. You don&rsquo;t get an unexpected good idea out of somebody following steps.</p>
<p><strong>The part that has to be practiced.</strong> None of that works without candor, and candor doesn&rsquo;t come free — for anyone. The pull toward smoothing things over is universal, and caring without saying the thing is the failure mode that feels kind and isn&rsquo;t. Take away the rules and keep the politeness and you don&rsquo;t get freedom. You get a team quietly drifting, with everyone guessing.</p>
<p>So I read <em>Radical Candor</em> as well, Kim Scott&rsquo;s book on exactly this — useful feedback needs both halves at once: you have to actually care about the person, and you have to actually say the thing. Hence this commitment, in writing, where anyone can hold me to it: expect me to be very clear about what I think, what I&rsquo;ve decided, and why — including when the answer is that I got it wrong.</p>
<p><strong>And yet — not a culture you can copy.</strong> One caveat over all of it. I don&rsquo;t want to import Netflix&rsquo;s culture wholesale. It grew out of a particular company, in a particular market, at a particular size, and plenty of what makes it work there doesn&rsquo;t travel. The book&rsquo;s famous sharp edge travels worst of all: the keeper test, where anyone a manager wouldn&rsquo;t fight to keep is let go with a generous severance. I&rsquo;d leave that part behind entirely. Talent density earned by hiring carefully is a different thing from talent density maintained by cutting — and an edge that hard has very American roots. It&rsquo;s not how I&rsquo;d want a small team in Denmark to feel. There&rsquo;s just as much worth taking from how good people at companies like Google or Apple have built and run their teams — and those cultures disagree with Netflix&rsquo;s in places, on purpose. Ours will be its own thing — worked out in Ry, not copied in from California. The real test of any of this is what everybody does when nobody&rsquo;s enforcing anything.</p>
<p>Which lands back where the expense policy started. Don&rsquo;t adopt somebody else&rsquo;s rulebook. Adopt the habit of asking what&rsquo;s genuinely best here, right now, with the people we&rsquo;ve actually got — and stay willing to change the answer when the situation does.</p>
<h2 id="a-note-to-future-me">A note to future me<a class="gn-heading-anchor" href="#a-note-to-future-me" aria-label="Link to this section"><span class="gn-ico" style="width:15px;height:15px"><!-- @license lucide-static v1.21.0 - ISC -->
<svg
  class="lucide lucide-link"
  xmlns="http://www.w3.org/2000/svg"
  width="24"
  height="24"
  viewBox="0 0 24 24"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
  <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</span></a>
</h2>
<p>By the time you read this, you&rsquo;ll know everything I don&rsquo;t. The start date. Whether those first ninety days went to plan. Which of these goals survived contact with reality, and which were sweetly naive. Some of it will look obvious in hindsight. Some of it will look funny.</p>
<p>I wanted it written down before the job could start shaping my answers — while it&rsquo;s all still conviction, before it hardens into experience.</p>
<p>Thomas already joked that my first long reply to him read like a novel. Consider this post the sequel. Longer, and this time public. Let&rsquo;s see how close I got.</p>
]]></content:encoded>
      <category>Culture</category>
      <category>AI</category>
    </item>
  </channel>
</rss>
