<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://waymarks.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://waymarks.net/" rel="alternate" type="text/html" /><updated>2026-04-08T09:22:33+00:00</updated><id>https://waymarks.net/feed.xml</id><title type="html">Waymarks</title><subtitle>A trail of tech discoveries</subtitle><entry><title type="html">Deploying NixOS to Hetzner EX44 dedicated server</title><link href="https://waymarks.net/2025/05/05/00-deploying-nixos-on-hetzner-ex44.html" rel="alternate" type="text/html" title="Deploying NixOS to Hetzner EX44 dedicated server" /><published>2025-05-05T00:00:00+00:00</published><updated>2025-05-05T00:00:00+00:00</updated><id>https://waymarks.net/2025/05/05/00-deploying-nixos-on-hetzner-ex44</id><content type="html" xml:base="https://waymarks.net/2025/05/05/00-deploying-nixos-on-hetzner-ex44.html"><![CDATA[<p>I like to use NixOS on my servers, it makes dealing with upgrades easy and provides a good deal of stability. The declarative configuration is a game changer - I can rebuild my server setup anywhere with minimal hassle.</p>

<p>I’ve recently purchased a Hetzner EX44 dedicated server, and despite a good number of blog or wiki articles about how to deploy NixOS on baremetal at Hetzner, I did not find one that worked out of the box for this specific model. So here is a quick recap of how I’ve managed to configure the server.</p>

<!--more-->

<h2 id="pre-deployment-planning">Pre-deployment Planning</h2>

<p>Before thinking of deploying NixOS, the first step is to understand what network and disk configuration will work for your server.</p>

<h2 id="networking-configuration">Networking Configuration</h2>

<p>Hetzner requires configuring static IP addresses, and its Robot dashboard provides everything that’s needed to do that.</p>

<h3 id="ipv4-configuration">IPv4 Configuration</h3>
<ul>
  <li><strong>IP</strong>: the server IP address</li>
  <li><strong>Gateway</strong>: the gateway address</li>
  <li><strong>Mask</strong>: the server subnet mask</li>
  <li><strong>Prefix</strong>: compute this from the subnet mask, ex: 255.255.255.224 -&gt; /27</li>
</ul>

<h3 id="ipv6-configuration">IPv6 Configuration</h3>
<ul>
  <li><strong>IP</strong>: the server IPv6 address</li>
  <li><strong>Gateway</strong>: the IPv6 gateway address</li>
  <li><strong>Prefix</strong>: usually /64</li>
</ul>

<h3 id="dns">DNS</h3>

<p>Do not forget to add DNS servers to the network config, this is quite useful if you need to debug the installation using the virtual KVM.</p>

<h3 id="firewall">Firewall</h3>

<p>It is good practice to configure the firewall to allow only the necessary ports. This is pretty standard but there is one gotcha: you will need to allow incoming ACK TCP packets so that your server can establish outside TCP connections.</p>

<h2 id="disk-setup">Disk Setup</h2>

<p>The bootloader setup that is supported on Hetzner servers is documented at <a href="https://docs.hetzner.com/robot/dedicated-server/operating-systems/efi-system-partition">Hetzner EFI System Partition</a>.
Basically, this boils down to creating an ESP partition that is at least 200MB large.</p>

<h2 id="installing-nixos-with-nixos-anywhere">Installing NixOS with nixos-anywhere</h2>

<p>In the past, I’ve personally installed NixOS on various computers using a multitude of methods. The last time I set up a dedicated server with NixOS was manually using kexec to boot a basic NixOS image and install NixOS from there.</p>

<p>With this new server, I’ve searched for new methods and found that <a href="https://github.com/nix-community/nixos-anywhere">nixos-anywhere</a> was the one I liked the most. First, it was easily reproducible; second, it integrates with disko, which handles disk partitioning; third, it actually works.</p>

<p>Here’s a sample configuration I used with nixos-anywhere:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># flake.nix</span>
<span class="p">{</span>
  <span class="nv">inputs</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">nixpkgs</span><span class="o">.</span><span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:NixOS/nixpkgs/nixos-unstable"</span><span class="p">;</span>
    <span class="nv">nixos-anywhere</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:nix-community/nixos-anywhere"</span><span class="p">;</span>
      <span class="nv">inputs</span><span class="o">.</span><span class="nv">nixpkgs</span><span class="o">.</span><span class="nv">follows</span> <span class="o">=</span> <span class="s2">"nixpkgs"</span><span class="p">;</span>
    <span class="p">};</span>
    <span class="nv">disko</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">url</span> <span class="o">=</span> <span class="s2">"github:nix-community/disko"</span><span class="p">;</span>
      <span class="nv">inputs</span><span class="o">.</span><span class="nv">nixpkgs</span><span class="o">.</span><span class="nv">follows</span> <span class="o">=</span> <span class="s2">"nixpkgs"</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>

  <span class="nv">outputs</span> <span class="o">=</span> <span class="p">{</span> <span class="nv">self</span><span class="p">,</span> <span class="nv">nixpkgs</span><span class="p">,</span> <span class="nv">nixos-anywhere</span><span class="p">,</span> <span class="nv">disko</span><span class="p">,</span> <span class="o">...</span> <span class="p">}:</span> <span class="p">{</span>
    <span class="nv">nixosConfigurations</span><span class="o">.</span><span class="nv">ex44</span> <span class="o">=</span> <span class="nv">nixpkgs</span><span class="o">.</span><span class="nv">lib</span><span class="o">.</span><span class="nv">nixosSystem</span> <span class="p">{</span>
      <span class="nv">system</span> <span class="o">=</span> <span class="s2">"x86_64-linux"</span><span class="p">;</span>
      <span class="nv">modules</span> <span class="o">=</span> <span class="p">[</span>
        <span class="sx">./configuration.nix</span>
        <span class="nv">disko</span><span class="o">.</span><span class="nv">nixosModules</span><span class="o">.</span><span class="nv">disko</span>
        <span class="sx">./disk-config.nix</span>
        <span class="sx">./network-config.nix</span>
      <span class="p">];</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For disk configuration, I created a simple disk-config.nix file setting up a raid1 partition for <code class="language-plaintext highlighter-rouge">/boot</code> and creating a mirror zfs pool for the rest of the disk:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># disk-config.nix</span>
<span class="p">{</span> <span class="o">...</span> <span class="p">}:</span>
<span class="kd">let</span>
  <span class="nv">mirrorBoot</span> <span class="o">=</span> <span class="nv">idx</span><span class="p">:</span> <span class="p">{</span>
    <span class="nv">type</span> <span class="o">=</span> <span class="s2">"disk"</span><span class="p">;</span>
    <span class="nv">device</span> <span class="o">=</span> <span class="s2">"/dev/nvme</span><span class="si">${</span><span class="nv">idx</span><span class="si">}</span><span class="s2">n1"</span><span class="p">;</span>
    <span class="nv">content</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">type</span> <span class="o">=</span> <span class="s2">"gpt"</span><span class="p">;</span>
      <span class="nv">partitions</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nv">ESP</span> <span class="o">=</span> <span class="p">{</span>
          <span class="nv">size</span> <span class="o">=</span> <span class="s2">"1G"</span><span class="p">;</span>
          <span class="nv">type</span> <span class="o">=</span> <span class="s2">"EF00"</span><span class="p">;</span>
          <span class="nv">content</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nv">type</span> <span class="o">=</span> <span class="s2">"mdraid"</span><span class="p">;</span>
            <span class="nv">name</span> <span class="o">=</span> <span class="s2">"boot"</span><span class="p">;</span>
          <span class="p">};</span>
        <span class="p">};</span>
        <span class="nv">zfs</span> <span class="o">=</span> <span class="p">{</span>
          <span class="nv">size</span> <span class="o">=</span> <span class="s2">"100%"</span><span class="p">;</span>
          <span class="nv">content</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nv">type</span> <span class="o">=</span> <span class="s2">"zfs"</span><span class="p">;</span>
            <span class="nv">pool</span> <span class="o">=</span> <span class="s2">"zroot"</span><span class="p">;</span>
          <span class="p">};</span>
        <span class="p">};</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="kn">in</span> <span class="p">{</span>
  <span class="nv">disko</span><span class="o">.</span><span class="nv">devices</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">disk</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">nvme0n1</span> <span class="o">=</span> <span class="nv">mirrorBoot</span> <span class="s2">"0"</span><span class="p">;</span>
      <span class="nv">nvme1n1</span> <span class="o">=</span> <span class="nv">mirrorBoot</span> <span class="s2">"1"</span><span class="p">;</span>
    <span class="p">};</span>

    <span class="nv">mdadm</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">boot</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nv">type</span> <span class="o">=</span> <span class="s2">"mdadm"</span><span class="p">;</span>
        <span class="nv">level</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
        <span class="nv">metadata</span> <span class="o">=</span> <span class="s2">"1.0"</span><span class="p">;</span>
        <span class="nv">content</span> <span class="o">=</span> <span class="p">{</span>
          <span class="nv">type</span> <span class="o">=</span> <span class="s2">"filesystem"</span><span class="p">;</span>
          <span class="nv">format</span> <span class="o">=</span> <span class="s2">"vfat"</span><span class="p">;</span>
          <span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"/boot"</span><span class="p">;</span>
          <span class="nv">mountOptions</span> <span class="o">=</span> <span class="p">[</span> <span class="s2">"umask=0077"</span> <span class="p">];</span>
        <span class="p">};</span>
      <span class="p">};</span>
    <span class="p">};</span>

    <span class="nv">zpool</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">zroot</span> <span class="o">=</span> <span class="p">{</span>
        <span class="nv">type</span> <span class="o">=</span> <span class="s2">"zpool"</span><span class="p">;</span>
        <span class="nv">mode</span> <span class="o">=</span> <span class="s2">"mirror"</span><span class="p">;</span>
        <span class="nv">rootFsOptions</span> <span class="o">=</span> <span class="p">{</span>
          <span class="nv">compression</span> <span class="o">=</span> <span class="s2">"lz4"</span><span class="p">;</span>
          <span class="nv">acltype</span> <span class="o">=</span> <span class="s2">"posixacl"</span><span class="p">;</span>
          <span class="nv">xattr</span> <span class="o">=</span> <span class="s2">"sa"</span><span class="p">;</span>
          <span class="s2">"com.sun:auto-snapshot"</span> <span class="o">=</span> <span class="s2">"true"</span><span class="p">;</span>
          <span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span>
        <span class="p">};</span>
        <span class="nv">datasets</span> <span class="o">=</span> <span class="p">{</span>
          <span class="s2">"root"</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nv">type</span> <span class="o">=</span> <span class="s2">"zfs_fs"</span><span class="p">;</span>
            <span class="nv">options</span> <span class="o">=</span> <span class="p">{</span> <span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span> <span class="p">};</span>
          <span class="p">};</span>
          <span class="s2">"root/nixos"</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nv">type</span> <span class="o">=</span> <span class="s2">"zfs_fs"</span><span class="p">;</span>
            <span class="nv">options</span><span class="o">.</span><span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"/"</span><span class="p">;</span>
            <span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"/"</span><span class="p">;</span>
          <span class="p">};</span>
          <span class="s2">"root/home"</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nv">type</span> <span class="o">=</span> <span class="s2">"zfs_fs"</span><span class="p">;</span>
            <span class="nv">options</span><span class="o">.</span><span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"/home"</span><span class="p">;</span>
            <span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"/home"</span><span class="p">;</span>
          <span class="p">};</span>
          <span class="s2">"root/tmp"</span> <span class="o">=</span> <span class="p">{</span>
            <span class="nv">type</span> <span class="o">=</span> <span class="s2">"zfs_fs"</span><span class="p">;</span>
            <span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"/tmp"</span><span class="p">;</span>
            <span class="nv">options</span> <span class="o">=</span> <span class="p">{</span>
              <span class="nv">mountpoint</span> <span class="o">=</span> <span class="s2">"/tmp"</span><span class="p">;</span>
              <span class="nv">sync</span> <span class="o">=</span> <span class="s2">"disabled"</span><span class="p">;</span>
            <span class="p">};</span>
          <span class="p">};</span>
        <span class="p">};</span>
      <span class="p">};</span>
    <span class="p">};</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The network configuration is also fairly straightforward:</p>

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># network-config.nix</span>
<span class="p">{</span> <span class="o">...</span> <span class="p">}:</span> <span class="p">{</span>
  <span class="nv">networking</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nv">hostId</span> <span class="o">=</span> <span class="s2">"a 32bit host id for zfs"</span><span class="p">;</span>

    <span class="nv">hostName</span> <span class="o">=</span> <span class="s2">"ex44"</span><span class="p">;</span>
    <span class="nv">useDHCP</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>

    <span class="c"># Disable predictable interface names because the server only has one network card and, ironically, it makes the configuration a bit more predictable.</span>
    <span class="nv">usePredictableInterfaceNames</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span>

    <span class="nv">interfaces</span><span class="o">.</span><span class="s2">"eth0"</span> <span class="o">=</span> <span class="p">{</span>
      <span class="nv">ipv4</span><span class="o">.</span><span class="nv">addresses</span> <span class="o">=</span> <span class="p">[{</span>
        <span class="nv">address</span> <span class="o">=</span> <span class="s2">"your.server.ip"</span><span class="p">;</span>
        <span class="nv">prefixLength</span> <span class="o">=</span> <span class="mi">27</span><span class="p">;</span> <span class="c"># computed from the mask</span>
      <span class="p">}];</span>
      <span class="nv">ipv6</span><span class="o">.</span><span class="nv">addresses</span> <span class="o">=</span> <span class="p">[{</span>
        <span class="nv">address</span> <span class="o">=</span> <span class="s2">"your:server:ipv6::address"</span><span class="p">;</span>
        <span class="nv">prefixLength</span> <span class="o">=</span> <span class="mi">64</span><span class="p">;</span>
      <span class="p">}];</span>
    <span class="p">};</span>
    <span class="nv">defaultGateway</span> <span class="o">=</span> <span class="s2">"your.gateway.ip"</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>To deploy using nixos-anywhere from the rescue system, I simply ran:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nix run github:nix-community/nixos-anywhere -- --flake .#ex44 --target-host root@your.server.ip --generate-hardware-config nixos-generate-config hardware-configuration.nix --disko-mode disko
</code></pre></div></div>

<p>In the end, following the nixos-anywhere <a href="https://nix-community.github.io/nixos-anywhere/quickstart.html">quickstart</a> tutorial with a configuration that matches the requirements we gathered earlier worked perfectly.</p>

<h2 id="post-installation-management">Post-Installation Management</h2>

<p>Once the server is up and running with NixOS, you can fall back to your preferred way of deploying it. As for myself, I use <a href="https://github.com/nlewo/comin">comin</a> which allows me to have a GitOps workflow for managing my servers. In a later article, I’ll explain how to automatically upgrade the flake inputs and also have a nice workflow to show the derivation diffs on PRs to the flake repository.</p>

<p>If you run into issues with your deployment, the Hetzner KVM console can be a lifesaver - it gives you direct console access even when networking isn’t working yet.</p>

<h1 id="acknowledgments">Acknowledgments</h1>

<p>I’ve mentioned reading a few articles on deploying Nixos to Hetzner servers and I’d like to list them here because they were super useful to me:</p>

<ul>
  <li><a href="https://mhu.dev/posts/2024-01-06-nixos-on-hetzner/">NixOS on Hetzner Dedicated</a> via <a href="http://mhu.dev">mhu.dev</a>.</li>
  <li><a href="https://wiki.nixos.org/wiki/Install_NixOS_on_Hetzner_Online">Install NixOS on Hetzner Online</a>.</li>
  <li><a href="https://github.com/Mic92/dotfiles/blob/296f9a0080a54a48e1601532caeccce99afbec6f/machines/eve/modules/disko.nix">Eve disko configuration</a> via <a href="https://github.com/Mic92/dotfiles">Mic92 dotfiles</a>.</li>
  <li><a href="https://github.com/nix-community/nixos-anywhere">nixos-anywhere</a>.</li>
</ul>]]></content><author><name></name></author><category term="nixos" /><category term="hetzner" /><category term="server" /><category term="linux" /><category term="devops" /><summary type="html"><![CDATA[I like to use NixOS on my servers, it makes dealing with upgrades easy and provides a good deal of stability. The declarative configuration is a game changer - I can rebuild my server setup anywhere with minimal hassle. I’ve recently purchased a Hetzner EX44 dedicated server, and despite a good number of blog or wiki articles about how to deploy NixOS on baremetal at Hetzner, I did not find one that worked out of the box for this specific model. So here is a quick recap of how I’ve managed to configure the server.]]></summary></entry><entry><title type="html">Practice of Reliable Software Design</title><link href="https://waymarks.net/2025/03/24/00-practice-of-reliable-software-design.html" rel="alternate" type="text/html" title="Practice of Reliable Software Design" /><published>2025-03-24T00:00:00+00:00</published><updated>2025-03-24T00:00:00+00:00</updated><id>https://waymarks.net/2025/03/24/00-practice-of-reliable-software-design</id><content type="html" xml:base="https://waymarks.net/2025/03/24/00-practice-of-reliable-software-design.html"><![CDATA[<p>In his article <a href="https://entropicthoughts.com/practices-of-reliable-software-design">Practice of Reliable Software Design</a>, kqr offers compelling perspectives on software design principles.</p>

<p>His thoughts on building reliable software carry significant implications for both developers and stakeholders alike:</p>

<blockquote>
  <p>It is much easier to add features to reliable software, than it is to add reliability to featureful software.</p>
</blockquote>

<p>This quote illustrates a common misconception in software development cycles. Many organizations misinterpret the principle of shipping early, viewing it as permission to rush out buggy features quickly. However, the value lies in delivering small, reliable components first, then iteratively building upon that stable foundation. This approach gives us practical insight into our design decisions and their real-world effectiveness. Another important but frequently neglected aspect kqr highlights is the importance of establishing clear boundaries and limits in our systems.</p>

<p>I’ve learned through experience that seemingly functional code often breaks down precisely because it lacks defined constraints, eventually depleting critical resources. When these issues emerge, implementing fixes becomes increasingly difficult and expensive. That’s why designing a system, should includes deliberate consideration of it’s intended capacity, imposing appropriate limitations rather than allowing unchecked growth.</p>

<p>The article also explores other significant aspects of reliable software design, such as the importance of testing, simplicity, and working with existing solutions. A thoroughly enjoyable and insightful read!</p>]]></content><author><name></name></author><category term="software-design" /><category term="reliability" /><category term="development" /><category term="architecture" /><category term="engineering-practices" /><summary type="html"><![CDATA[In his article Practice of Reliable Software Design, kqr offers compelling perspectives on software design principles. His thoughts on building reliable software carry significant implications for both developers and stakeholders alike: It is much easier to add features to reliable software, than it is to add reliability to featureful software. This quote illustrates a common misconception in software development cycles. Many organizations misinterpret the principle of shipping early, viewing it as permission to rush out buggy features quickly. However, the value lies in delivering small, reliable components first, then iteratively building upon that stable foundation. This approach gives us practical insight into our design decisions and their real-world effectiveness. Another important but frequently neglected aspect kqr highlights is the importance of establishing clear boundaries and limits in our systems. I’ve learned through experience that seemingly functional code often breaks down precisely because it lacks defined constraints, eventually depleting critical resources. When these issues emerge, implementing fixes becomes increasingly difficult and expensive. That’s why designing a system, should includes deliberate consideration of it’s intended capacity, imposing appropriate limitations rather than allowing unchecked growth. The article also explores other significant aspects of reliable software design, such as the importance of testing, simplicity, and working with existing solutions. A thoroughly enjoyable and insightful read!]]></summary></entry><entry><title type="html">Packaging Claude Code on NixOS</title><link href="https://waymarks.net/2025/03/02/00-claude-code-on-nixos.html" rel="alternate" type="text/html" title="Packaging Claude Code on NixOS" /><published>2025-03-02T00:00:00+00:00</published><updated>2025-03-02T00:00:00+00:00</updated><id>https://waymarks.net/2025/03/02/00-claude-code-on-nixos</id><content type="html" xml:base="https://waymarks.net/2025/03/02/00-claude-code-on-nixos.html"><![CDATA[<p>I’ve been heavily using claude-code since its release and have found it to be one of the most powerful LLM-assisted coding tools on the market today.</p>

<p>There was just one problem: it’s not available on NixOS which my home computer is running on. I decided to package it myself.</p>

<p>Fortunately, the packaging process turned out to be relatively straightforward, though with a few interesting challenges. Since claude-code isn’t open-source, we need to work with the npm-hosted version.</p>

<p>I’m documenting my packaging approach below to help fellow NixOS users who want to use this tool.</p>

<!--more-->

<div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="nv">lib</span><span class="p">,</span> <span class="nv">buildNpmPackage</span><span class="p">,</span> <span class="nv">fetchurl</span><span class="p">,</span> <span class="nv">nodejs</span><span class="p">,</span> <span class="nv">makeWrapper</span><span class="p">,</span> <span class="nv">writeShellScriptBin</span> <span class="p">}:</span>

<span class="kd">let</span>
  <span class="c"># Main package</span>
  <span class="nv">claudeCode</span> <span class="o">=</span> <span class="nv">buildNpmPackage</span> <span class="kr">rec</span> <span class="p">{</span>
    <span class="nv">pname</span> <span class="o">=</span> <span class="s2">"claude-code"</span><span class="p">;</span>
    <span class="nv">version</span> <span class="o">=</span> <span class="s2">"0.2.29"</span><span class="p">;</span>

    <span class="nv">src</span> <span class="o">=</span> <span class="nv">fetchurl</span> <span class="p">{</span>
      <span class="nv">url</span> <span class="o">=</span> <span class="s2">"https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-</span><span class="si">${</span><span class="nv">version</span><span class="si">}</span><span class="s2">.tgz"</span><span class="p">;</span>
      <span class="nv">hash</span> <span class="o">=</span> <span class="s2">"sha256-1iKDtTE+cHXMW/3zxfsNFjMGMxJlIBzGEXWtTfQfSMM="</span><span class="p">;</span>
    <span class="p">};</span>

    <span class="nv">npmDepsHash</span> <span class="o">=</span> <span class="s2">"sha256-fuJE/YTd9apAd1cooxgHQwPda5js44EmSfjuRVPbKdM="</span><span class="p">;</span>

    <span class="kn">inherit</span> <span class="nv">nodejs</span><span class="p">;</span>

    <span class="nv">makeCacheWritable</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>

    <span class="nv">postPatch</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">      if [ -f "</span><span class="si">${</span><span class="sx">./claude-code/package-lock.json</span><span class="si">}</span><span class="s2">" ]; then</span><span class="err">
</span><span class="s2">        echo "Using vendored package-lock.json"</span><span class="err">
</span><span class="s2">        cp "</span><span class="si">${</span><span class="sx">./claude-code/package-lock.json</span><span class="si">}</span><span class="s2">" ./package-lock.json</span><span class="err">
</span><span class="s2">      else</span><span class="err">
</span><span class="s2">        echo "No vendored package-lock.json found, creating a minimal one"</span><span class="err">
</span><span class="s2">        exit 1</span><span class="err">
</span><span class="s2">      fi</span><span class="err">
</span><span class="s2">    ''</span><span class="p">;</span>

    <span class="nv">dontNpmBuild</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
    <span class="nv">dontNpmInstall</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>

    <span class="nv">nativeBuildInputs</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">makeWrapper</span> <span class="p">];</span>

    <span class="c"># Create a custom installation phase to handle the package organization</span>
    <span class="nv">installPhase</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">      # Create a directory for the lib files</span><span class="err">
</span><span class="s2">      mkdir -p $out/lib/node_modules/@anthropic-ai/claude-code</span><span class="err">

</span><span class="s2">      # Copy all package files to the lib directory</span><span class="err">
</span><span class="s2">      cp -a . $out/lib/node_modules/@anthropic-ai/claude-code/</span><span class="err">

</span><span class="s2">      # Create bin directory</span><span class="err">
</span><span class="s2">      mkdir -p $out/bin</span><span class="err">

</span><span class="s2">      # Create a wrapper script that points to the actual CLI script</span><span class="err">
</span><span class="s2">      makeWrapper </span><span class="si">${</span><span class="nv">nodejs</span><span class="si">}</span><span class="s2">/bin/node $out/bin/claude-code \</span><span class="err">
</span><span class="s2">        --add-flags "$out/lib/node_modules/@anthropic-ai/claude-code/cli.mjs"</span><span class="err">
</span><span class="s2">    ''</span><span class="p">;</span>

    <span class="nv">meta</span> <span class="o">=</span> <span class="kn">with</span> <span class="nv">lib</span><span class="p">;</span> <span class="p">{</span>
      <span class="nv">description</span> <span class="o">=</span> <span class="s2">"Claude Code CLI tool"</span><span class="p">;</span>
      <span class="nv">homepage</span> <span class="o">=</span> <span class="s2">"https://www.anthropic.com/claude-code"</span><span class="p">;</span>
      <span class="nv">mainProgram</span> <span class="o">=</span> <span class="s2">"claude-code"</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">};</span>

  <span class="c"># Helper script to update the package-lock.json file</span>
  <span class="c">#</span>
  <span class="c"># Build with `nix build .#claude-code.updateScript`</span>
  <span class="nv">updateScript</span> <span class="o">=</span> <span class="nv">writeShellScriptBin</span> <span class="s2">"update-claude-code-lock"</span> <span class="s2">''</span><span class="err">
</span><span class="s2">    #!/usr/bin/env bash</span><span class="err">
</span><span class="s2">    set -e</span><span class="err">

</span><span class="s2">    if [ $# -ne 1 ]; then</span><span class="err">
</span><span class="s2">      echo "Usage: update-claude-code-lock &lt;version&gt;"</span><span class="err">
</span><span class="s2">      echo "Example: update-claude-code-lock 0.2.29"</span><span class="err">
</span><span class="s2">      exit 1</span><span class="err">
</span><span class="s2">    fi</span><span class="err">

</span><span class="s2">    VERSION="$1"</span><span class="err">
</span><span class="s2">    TEMP_DIR=$(mktemp -d)</span><span class="err">
</span><span class="s2">    LOCK_DIR="$PWD/packages/claude-code"</span><span class="err">

</span><span class="s2">    echo "Creating $LOCK_DIR if it doesn't exist..."</span><span class="err">
</span><span class="s2">    mkdir -p "$LOCK_DIR"</span><span class="err">

</span><span class="s2">    echo "Downloading claude-code version $VERSION..."</span><span class="err">
</span><span class="s2">    curl -L "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-$VERSION.tgz" -o "$TEMP_DIR/claude-code.tgz"</span><span class="err">

</span><span class="s2">    echo "Extracting tarball..."</span><span class="err">
</span><span class="s2">    mkdir -p "$TEMP_DIR/extract"</span><span class="err">
</span><span class="s2">    tar -xzf "$TEMP_DIR/claude-code.tgz" -C "$TEMP_DIR/extract"</span><span class="err">

</span><span class="s2">    echo "Generating package-lock.json..."</span><span class="err">
</span><span class="s2">    cd "$TEMP_DIR/extract/package"</span><span class="err">
</span><span class="s2">    </span><span class="si">${</span><span class="nv">nodejs</span><span class="si">}</span><span class="s2">/bin/npm install --package-lock-only --ignore-scripts</span><span class="err">

</span><span class="s2">    echo "Copying package-lock.json to $LOCK_DIR..."</span><span class="err">
</span><span class="s2">    cp package-lock.json "$LOCK_DIR/"</span><span class="err">

</span><span class="s2">    echo "Cleaning up..."</span><span class="err">
</span><span class="s2">    rm -rf "$TEMP_DIR"</span><span class="err">

</span><span class="s2">    echo "Done. Package lock file updated at $LOCK_DIR/package-lock.json"</span><span class="err">
</span><span class="s2">    echo "You may need to update the npmDepsHash in your claude-code.nix file."</span><span class="err">
</span><span class="s2">    echo "Use: prefetch-npm-deps $LOCK_DIR/package-lock.json"</span><span class="err">
</span><span class="s2">  ''</span><span class="p">;</span>
<span class="kn">in</span>
  <span class="c"># Return both the package and the update script</span>
  <span class="nv">claudeCode</span> <span class="o">//</span> <span class="p">{</span>
    <span class="nv">updateScript</span> <span class="o">=</span> <span class="nv">updateScript</span><span class="p">;</span>
    <span class="nv">passthru</span> <span class="o">=</span> <span class="p">(</span><span class="nv">claudeCode</span><span class="o">.</span><span class="nv">passthru</span> <span class="nv">or</span> <span class="p">{})</span> <span class="o">//</span> <span class="p">{</span>
      <span class="kn">inherit</span> <span class="nv">updateScript</span><span class="p">;</span>
    <span class="p">};</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>Here are the few highlights about the packaging process:</p>

<ol>
  <li>
    <p>The package needs to be sourced directly from the npm registry:</p>

    <div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     <span class="nv">src</span> <span class="o">=</span> <span class="nv">fetchurl</span> <span class="p">{</span>
         <span class="nv">url</span> <span class="o">=</span> <span class="s2">"https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-</span><span class="si">${</span><span class="nv">version</span><span class="si">}</span><span class="s2">.tgz"</span><span class="p">;</span>
         <span class="nv">hash</span> <span class="o">=</span> <span class="s2">"sha256-1iKDtTE+cHXMW/3zxfsNFjMGMxJlIBzGEXWtTfQfSMM="</span><span class="p">;</span>
     <span class="p">};</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>We vendor the package-json.lock file because the source does not include it.</p>

    <div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     <span class="nv">postPatch</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">     if [ -f "</span><span class="si">${</span><span class="sx">./claude-code/package-lock.json</span><span class="si">}</span><span class="s2">" ]; then</span><span class="err">
</span><span class="s2">         echo "Using vendored package-lock.json"</span><span class="err">
</span><span class="s2">         cp "</span><span class="si">${</span><span class="sx">./claude-code/package-lock.json</span><span class="si">}</span><span class="s2">" ./package-lock.json</span><span class="err">
</span><span class="s2">     else</span><span class="err">
</span><span class="s2">         echo "No vendored package-lock.json found, creating a minimal one"</span><span class="err">
</span><span class="s2">         exit 1</span><span class="err">
</span><span class="s2">     fi</span><span class="err">
</span><span class="s2">     ''</span><span class="p">;</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>To make future updates easier, I’ve included a helper script that automatically generates the package-lock.json file from the npm registry:</p>

    <div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c"># Helper script to update the package-lock.json file</span>
 <span class="c">#</span>
 <span class="c"># Build with `nix build .#claude-code.updateScript`</span>
 <span class="nv">updateScript</span> <span class="o">=</span> <span class="nv">writeShellScriptBin</span> <span class="s2">"update-claude-code-lock"</span> <span class="s2">''</span><span class="err">
</span><span class="s2"> #!/usr/bin/env bash</span><span class="err">
</span><span class="s2"> set -e</span><span class="err">

</span><span class="s2"> if [ $# -ne 1 ]; then</span><span class="err">
</span><span class="s2">     echo "Usage: update-claude-code-lock &lt;version&gt;"</span><span class="err">
</span><span class="s2">     echo "Example: update-claude-code-lock 0.2.29"</span><span class="err">
</span><span class="s2">     exit 1</span><span class="err">
</span><span class="s2"> fi</span><span class="err">

</span><span class="s2"> VERSION="$1"</span><span class="err">
</span><span class="s2"> TEMP_DIR=$(mktemp -d)</span><span class="err">
</span><span class="s2"> LOCK_DIR="$PWD/packages/claude-code"</span><span class="err">

</span><span class="s2"> echo "Creating $LOCK_DIR if it doesn't exist..."</span><span class="err">
</span><span class="s2"> mkdir -p "$LOCK_DIR"</span><span class="err">

</span><span class="s2"> echo "Downloading claude-code version $VERSION..."</span><span class="err">
</span><span class="s2"> curl -L "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-$VERSION.tgz" -o "$TEMP_DIR/claude-code.tgz"</span><span class="err">

</span><span class="s2"> echo "Extracting tarball..."</span><span class="err">
</span><span class="s2"> mkdir -p "$TEMP_DIR/extract"</span><span class="err">
</span><span class="s2"> tar -xzf "$TEMP_DIR/claude-code.tgz" -C "$TEMP_DIR/extract"</span><span class="err">

</span><span class="s2"> echo "Generating package-lock.json..."</span><span class="err">
</span><span class="s2"> cd "$TEMP_DIR/extract/package"</span><span class="err">
</span><span class="s2"> </span><span class="si">${</span><span class="nv">nodejs</span><span class="si">}</span><span class="s2">/bin/npm install --package-lock-only --ignore-scripts</span><span class="err">

</span><span class="s2"> echo "Copying package-lock.json to $LOCK_DIR..."</span><span class="err">
</span><span class="s2"> cp package-lock.json "$LOCK_DIR/"</span><span class="err">

</span><span class="s2"> echo "Cleaning up..."</span><span class="err">
</span><span class="s2"> rm -rf "$TEMP_DIR"</span><span class="err">

</span><span class="s2"> echo "Done. Package lock file updated at $LOCK_DIR/package-lock.json"</span><span class="err">
</span><span class="s2"> echo "You may need to update the npmDepsHash in your claude-code.nix file."</span><span class="err">
</span><span class="s2"> echo "Use: prefetch-npm-deps $LOCK_DIR/package-lock.json"</span><span class="err">
</span><span class="s2"> ''</span><span class="p">;</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>The standard npm install phase doesn’t work for this package, so we implement a custom approach:</p>

    <div class="language-nix highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     <span class="nv">dontNpmBuild</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>
     <span class="nv">dontNpmInstall</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span>

     <span class="nv">nativeBuildInputs</span> <span class="o">=</span> <span class="p">[</span> <span class="nv">makeWrapper</span> <span class="p">];</span>

     <span class="c"># Create a custom installation phase to handle the package organization</span>
     <span class="nv">installPhase</span> <span class="o">=</span> <span class="s2">''</span><span class="err">
</span><span class="s2">         # Create a directory for the lib files</span><span class="err">
</span><span class="s2">         mkdir -p $out/lib/node_modules/@anthropic-ai/claude-code</span><span class="err">

</span><span class="s2">         # Copy all package files to the lib directory</span><span class="err">
</span><span class="s2">         cp -a . $out/lib/node_modules/@anthropic-ai/claude-code/</span><span class="err">

</span><span class="s2">         # Create bin directory</span><span class="err">
</span><span class="s2">         mkdir -p $out/bin</span><span class="err">

</span><span class="s2">         # Create a wrapper script that points to the actual CLI script</span><span class="err">
</span><span class="s2">         makeWrapper </span><span class="si">${</span><span class="nv">nodejs</span><span class="si">}</span><span class="s2">/bin/node $out/bin/claude-code \</span><span class="err">
</span><span class="s2">             --add-flags "$out/lib/node_modules/@anthropic-ai/claude-code/cli.mjs"</span><span class="err">
</span><span class="s2">     ''</span><span class="p">;</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>This wrapper script approach keeps the binary directory clean while ensuring the CLI has access to all its required dependencies.</p>

<p><strong>EDIT(2025-03-03):</strong> <em>phanirithvij</em> rightfully <a href="https://discourse.nixos.org/t/packaging-claude-code-on-nixos/61072/5">pointed out</a> that the package has been merged to nixos-unstable: <a href="https://github.com/NixOS/nixpkgs/pull/384860">claude-code: init at 0.2.9 by malob</a></p>]]></content><author><name></name></author><category term="nixos" /><category term="claude-code" /><category term="packaging" /><category term="anthropic" /><category term="linux" /><category term="npm" /><category term="javascript" /><summary type="html"><![CDATA[I’ve been heavily using claude-code since its release and have found it to be one of the most powerful LLM-assisted coding tools on the market today. There was just one problem: it’s not available on NixOS which my home computer is running on. I decided to package it myself. Fortunately, the packaging process turned out to be relatively straightforward, though with a few interesting challenges. Since claude-code isn’t open-source, we need to work with the npm-hosted version. I’m documenting my packaging approach below to help fellow NixOS users who want to use this tool.]]></summary></entry><entry><title type="html">rails-diff - compare rails generated files</title><link href="https://waymarks.net/2025/02/28/00-rails-diff.html" rel="alternate" type="text/html" title="rails-diff - compare rails generated files" /><published>2025-02-28T00:00:00+00:00</published><updated>2025-02-28T00:00:00+00:00</updated><id>https://waymarks.net/2025/02/28/00-rails-diff</id><content type="html" xml:base="https://waymarks.net/2025/02/28/00-rails-diff.html"><![CDATA[<p><a href="https://github.com/MatheusRich/rails-diff">rails-diff</a> via <a href="https://rubyweekly.com/issues/740">Ruby Weekly #740</a> is a tool which allows comparing the files generated by Rails during the installation or upgrade process with their current version in the local repository.</p>

<p>When dealing with Rails upgrades, or with generator <code class="language-plaintext highlighter-rouge">*:install</code> commands, I often find that figuring out the diffs and what needs to get updated or removed is the least enjoyable part of the process.</p>

<p>I have yet to try this tool, but lately I’ve been thinking of a few ways to improve this process:</p>
<ol>
  <li>Make the diff easier to deal with by adding your own edits at the end of the generated files, unless changing a generated option value.</li>
  <li>Move your own changes to dedicated files which, by limiting the changes to the generated files to a minimum, would make dealing with diffs easier.</li>
</ol>

<p>Either way, I’ll try these approaches on our applications at work and hopefully make upgrading Rails an enjoyable experience.</p>]]></content><author><name></name></author><category term="ruby" /><category term="rails" /><category term="tools" /><category term="development" /><category term="upgrades" /><summary type="html"><![CDATA[rails-diff via Ruby Weekly #740 is a tool which allows comparing the files generated by Rails during the installation or upgrade process with their current version in the local repository. When dealing with Rails upgrades, or with generator *:install commands, I often find that figuring out the diffs and what needs to get updated or removed is the least enjoyable part of the process. I have yet to try this tool, but lately I’ve been thinking of a few ways to improve this process: Make the diff easier to deal with by adding your own edits at the end of the generated files, unless changing a generated option value. Move your own changes to dedicated files which, by limiting the changes to the generated files to a minimum, would make dealing with diffs easier. Either way, I’ll try these approaches on our applications at work and hopefully make upgrading Rails an enjoyable experience.]]></summary></entry><entry><title type="html">Honyaku - OpenAI powered rails translations</title><link href="https://waymarks.net/2025/02/28/01-honyaku.html" rel="alternate" type="text/html" title="Honyaku - OpenAI powered rails translations" /><published>2025-02-28T00:00:00+00:00</published><updated>2025-02-28T00:00:00+00:00</updated><id>https://waymarks.net/2025/02/28/01-honyaku</id><content type="html" xml:base="https://waymarks.net/2025/02/28/01-honyaku.html"><![CDATA[<p><a href="https://github.com/andrewculver/honyaku">Honyaku</a> recently featured in <a href="https://rubyweekly.com/issues/740">Ruby Weekly #740</a> is a tool built with LLM-powered tools (<a href="https://cursor.com">Cursor</a> and Claude Sonnet 3.5) to automatically translate Rails locale files.</p>

<p>While automated translation isn’t revolutionary on its own, Honyaku’s business impact is striking:</p>

<blockquote>
  <p>Created because it replaced a $34K/year SaaS contract and streamlined our deploy process.</p>
</blockquote>

<p>This case perfectly illustrates the power of focused software development: building a targeted solution to a specific problem can drastically cut costs.</p>

<p>It’s fascinating how this practical approach often delivers better, more efficient results than using off-the-shelf, expensive services.</p>

<p>From my experience, this problem-first mindset typically leads to solutions that are both simpler and more economical.</p>]]></content><author><name></name></author><category term="ruby" /><category term="rails" /><category term="translations" /><category term="i18n" /><category term="openai" /><category term="llm" /><category term="cost-savings" /><summary type="html"><![CDATA[Honyaku recently featured in Ruby Weekly #740 is a tool built with LLM-powered tools (Cursor and Claude Sonnet 3.5) to automatically translate Rails locale files. While automated translation isn’t revolutionary on its own, Honyaku’s business impact is striking: Created because it replaced a $34K/year SaaS contract and streamlined our deploy process. This case perfectly illustrates the power of focused software development: building a targeted solution to a specific problem can drastically cut costs. It’s fascinating how this practical approach often delivers better, more efficient results than using off-the-shelf, expensive services. From my experience, this problem-first mindset typically leads to solutions that are both simpler and more economical.]]></summary></entry><entry><title type="html">Prosopite - Rails n+1 query auto-detection</title><link href="https://waymarks.net/2025/02/28/02-prosopite.html" rel="alternate" type="text/html" title="Prosopite - Rails n+1 query auto-detection" /><published>2025-02-28T00:00:00+00:00</published><updated>2025-02-28T00:00:00+00:00</updated><id>https://waymarks.net/2025/02/28/02-prosopite</id><content type="html" xml:base="https://waymarks.net/2025/02/28/02-prosopite.html"><![CDATA[<p><a href="https://github.com/charkost/prosopite">Prosopite</a>, recently featured in <a href="https://rubyweekly.com/issues/740">Ruby Weekly #740</a>, emerges as a promising alternative to the well-known <a href="https://github.com/flyerhzm/bullet">Bullet</a> gem. This newer tool offers a more streamlined approach to detecting n+1 queries in Rails applications while boasting improved accuracy with fewer false-positives.</p>

<p>N+1 queries often significantly impact application performance. Having reliable detection tools in our arsenal is essential for maintaining efficient database interactions.</p>

<p>I’ve recently dealt with removing N+1 queries on an application, leading to 2x latency improvements. That system also suffered from another kind of N+1 problem - it looked up a ton of cache keys in Redis, which I’ve changed to a single lookup, avoiding unnecessary roundtrips to the cache.</p>]]></content><author><name></name></author><category term="ruby" /><category term="rails" /><category term="performance" /><category term="n+1-queries" /><category term="database" /><category term="optimization" /><summary type="html"><![CDATA[Prosopite, recently featured in Ruby Weekly #740, emerges as a promising alternative to the well-known Bullet gem. This newer tool offers a more streamlined approach to detecting n+1 queries in Rails applications while boasting improved accuracy with fewer false-positives. N+1 queries often significantly impact application performance. Having reliable detection tools in our arsenal is essential for maintaining efficient database interactions. I’ve recently dealt with removing N+1 queries on an application, leading to 2x latency improvements. That system also suffered from another kind of N+1 problem - it looked up a ton of cache keys in Redis, which I’ve changed to a single lookup, avoiding unnecessary roundtrips to the cache.]]></summary></entry><entry><title type="html">olmOCR</title><link href="https://waymarks.net/2025/02/26/olmocr.html" rel="alternate" type="text/html" title="olmOCR" /><published>2025-02-26T00:00:00+00:00</published><updated>2025-02-26T00:00:00+00:00</updated><id>https://waymarks.net/2025/02/26/olmocr</id><content type="html" xml:base="https://waymarks.net/2025/02/26/olmocr.html"><![CDATA[<p>In <a href="https://simonwillison.net/2025/Feb/26/olmocr/#atom-everything">olmOCR</a>, <a href="https://simonwillison.net">Simon Willison</a> shares an interesting piece of software. OCR is, as far as I know, a resource-heavy and costly process. Unfortunately, the article doesn’t mention the latency of the process. I find the concept of “document anchoring” really interesting, and I appreciate that they released a training dataset, which is still relatively rare these days.</p>]]></content><author><name></name></author><category term="ocr" /><category term="ai" /><category term="machine-learning" /><category term="document-processing" /><summary type="html"><![CDATA[In olmOCR, Simon Willison shares an interesting piece of software. OCR is, as far as I know, a resource-heavy and costly process. Unfortunately, the article doesn’t mention the latency of the process. I find the concept of “document anchoring” really interesting, and I appreciate that they released a training dataset, which is still relatively rare these days.]]></summary></entry></feed>